Приветствую! Меня зовут Андрей Степанов, я CTO во fuse8. Мне интересно знакомиться с опытом коллег по цеху и делиться своим. В сфере я уже больше 20 лет. В этой статье – небольшое погружение в задачу по повышению производительности крупного сайта, много полезных ссылок и инструментов, которые вы сможете использовать для своих проектов.

Один из наших проектов – сайт крупной компании по продаже автозапчастей и комплектующих. Это интернет-магазин, аудитория которого насчитывает около 500 тысяч уникальных пользователей в месяц. Оптимизировать нужно было главную страницу этого интернет-магазина.

На сайте, на которым велась работа, используется APM сервис elastic – система сбора метрик, согласно которым можно отследить производительность ресурса. Так как сайт долгое время не оптимизировался, а только дополнялся новыми фичами, показатели стали падать. 

Не все показатели пока удалось вытянуть в зеленую зону, но улучшение для многих из них получилось кратным. Пока дальнейшие работы планируются, расскажем, какие шаги предприняли для текущего результата.   

TS-Prune

Для начала используем сервис, который позволяет быстро найти мертвые экспорты или код в проекте.

Пример команды для package.json:

"deadcode-check": "npx --yes ts-"deadcode-check": "npx --yes ts-prune -s \"pages/[**/]?
(_app|_document|_error|index)|store/(index|sagas)|styles/global\""

Такой результат получаем: 

После отработки в консоле будет список проблемных мест. По ним проходимся вручную и удаляем мертвые сегменты. 

Depcheck

Есть и другой пакет для оптимизации. С его помощью получаем список неиспользуемых npm пакетов. Затем вручную проходимся по списку и удаляем все лишнее, тем самым уменьшая вес проекта и наводя порядок.  

Depcheck нужно использовать несколько раз, потому как после каждого прогона и удаления ненужных библиотек могут появится новые, которые становятся рудиментарными после первого прогона. Например, удаленная в первой очереди библиотека использовала другую, которая уже на втором прогоне определится как мертвая зависимость. 

Поиск дублирующих npm пакетов

Используем плагин, который анализирует проект и показывает список дублей с разными версиями.

Вот пример результата его работы на примере проекта сортировки:

Однако от видов выдачи результатов может зависеть алгоритм дальнейших действий. 

Например, вот такая выдача:

Здесь явно понимаем, что нужно обновить пакеты. Обновив зависимости, можно будет уменьшить вес бандла. 

Другой пример:

В этом случае также требуются обновления. Если не понятно, что и где обновлять, в настройках вебпака добавляем alias, который подскажет сборщику, какую именно версию обновить и откуда ее взять. На примере redux:

Оптимизация картинок

Картинки на сайте до оптимизации не масштабировались в зависимости от размеров экрана. Мы поменяли все картинки на next image. Затем поменяли приоритеты загрузки. Те, что первыми попадают во viewport, должны загружаться с высоким приоритетом и без lazyloading – то есть максимально быстро. Это влияет на скорость отрисовки сайта. 

  1. Используем для всех картинок модуль next/image.

  2. Добавляем современные форматы изображений вместо jpg и png. Получаем лучшее сжатие без потери качества и более быструю загрузку.

images: { 
formats: ['image/avif', 'image/webp'], 
}
  1. Приоритезируем загрузку картинок above the fold (тех, что находятся в viewport при первоначальной загрузке страницы) - свойство Priority.

  2.  Для векторных картинок выставляем свойство unoptimized.

<Image
	scr="/static/icons/mainPage/qualityControl.svg"
	width={40}
	height={40}
	unoptimized
	priority
	alt="Quality control"
     />

Code splitting

Выяснилось, что, в коде на каждой странице много дубликатов. Элементы, которые должны один раз загрузиться, закэшироваться в браузере и использоваться всеми страницами, были вставлены в код каждой из страниц и потребляли ресурсы при загрузке снова и снова.

Отчет webpack-bundle-analyzer. Красным выделены повторяющиеся модули.
Отчет webpack-bundle-analyzer. Красным выделены повторяющиеся модули.

Дубликаты элементов в коде появились из-за отмены дефолтного разбиения кода на чанки в next.js. Прежние разработчики применили такое решение, чтобы работала Linaria, которая призвана увеличивать производительность. Однако из-за отключения разбиения производительность наоборот падала. 

В итоге мы убрали строку, которая блокирует разбиение чанков, и это дало практически 50% прирост в производительности. 

Встроенный механизм next разбивки js кода на чанки протестирован и рекомендован для production. Отключать его не нужно, иначе код начинает дублироваться для каждой страницы, хотя переиспользуемые модули (react, react-dom, ui kit и т.д.) должны выноситься в отдельные (общие) чанки, а не грузиться заново на каждой странице. 

Так делать нельзя:

// next.config.js 
webpack: (config, { isServer }) => { 
config.optimization.splitChunks = false; // 
return config;
}

Убираем блокирующие ресурсы

Далее использовали webpagetest, на котором можно запускать тесты на производительность сайтов и получать разные метрики. После теста выяснилось, что в коде сайта есть блокирующие скрипты. 

Блокирующий скрипт стали загружать асинхронно и только на тех страницах, где это было необходимо. Синхронная загрузка блокирует очередь загрузки. Все скрипты должны быть не блокирующими.

Пример с aplaut:

Компактные сборки

Вместо Terser используем swcMinify для сжатия js (на крупном проекте экономия порядка 200Kb).

// next.config.js 
module.exports = { 
swcMinify: true, 
}

Собираем js только для современных браузеров. Список браузеров по умолчанию в Next, можно переопределить в своем browserslist.

"chrome 61", 
"edge 16", 
"firefox 60", 
"opera 48", 
"safari 11"
// next.config.js 

module.exports = { 
experimental: { 
legacyBrowsers: false, 
}, 
} 

Сторонние скрипты

Чтобы сократить общее время блокировки (TBT), сторонние скрипты для сайта (например, Google analytics и Yandex metrika) должны быть подключены с использованием next/script и соответствующих стратегий загрузки (чаще всего afterInteractive).

Вот пример подключения  Google Analytics c использованием next/script.

Скрытые компоненты

На сайте есть компоненты, которые скрыты до того момента, пока пользователь не начнет с ними взаимодействовать. Например, это компоненты в модальных окнах и сайдбарах. Для них используем Dynamic Imports, чтобы уменьшить объем js, необходимый для первоначальной загрузки страницы. Загрузка js скрытого компонента откладывается до момента пользовательской потребности – взаимодействия с компонентом. 

Ускорение билда

Можно отключить проверку eslint в next.config.js. Если линт отрабатывает на этапе комита, делать линт при билде нет смысла. Этот нюанс не влияет на пользователя, но увеличивает скорость сборки.

module.exports = { 
eslint: { 
// Warning: This allows production builds to successfully complete even if // your project has ESLint errors. 
ignoreDuringBuilds: true, 
}, 
}

Также можно использовать параметр --no-lint в package.json:

"scripts": { 
"dev": "ts-node server.ts", 
"build": "next build --no-lint", 
"build-analyze": "rimraf .next && cross-env ANALYZE=true next build", 
"start": "cross-env ts-node server.ts", 
"lint": "tsc && eslint **/*.{js,jsx,ts,tsx} --fix" 
}

Что еще полезно

Если работа на проекте ведется регулярно, сопровождаясь частыми релизами, стоит подумать о настройке мониторингов производительности после каждого релиза. Так, после внедрения новых фич, можно будет сразу отрабатывать проседания показателей. Это гораздо удобнее, чем дожидаться критического падения производительности и оптимизировать все целиком.

А чтобы в целом держать руку на пульсе, советуем чек-лист по улучшению производительности страниц от Виталия Фридмана. Чек-лист меняется каждый год, поэтому советы и подходы в нем получаются максимально актуальными и пригодными для использования в проектах.

За подготовку и содействие в составлении материала выражаю большую благодарность фронтенд-разработчику Сергею Пестову.

Комментарии (11)


  1. Opaspap
    17.08.2023 08:18
    +1

    1.1 сек многовато, если с кэшом. Но насколько я помню, эта хромовская тулза вообще плохо кэш учитывает, у меня приложение с загрузкой в 300мс медианы получало очень стремные скоры от маяка этого, просто потому что выкидывало service worker и его кэш :)


    1. andrey_stepanov1 Автор
      17.08.2023 08:18

      Webpagetest умеет проверять повторные загрузки страницы с кешем браузера, но его работу с service worker мы не проверяли.


  1. nin-jin
    17.08.2023 08:18
    +1

    Оно уже на проде? А то я вижу там на главной такую картину:

    Как-то очень не очень.
    Как-то очень не очень.


    1. andrey_stepanov1 Автор
      17.08.2023 08:18

      Да, в проде - но где не могу сказать по NDA :)


      1. nin-jin
        17.08.2023 08:18

        То есть следующие моменты вас не смутили при тестировании довольно простой главной страницы, я правильно понимаю?

        • 30 скриптов, общим объёмом в пол мегабайта в сжатом виде.

        • Загрузка множества мелких файлов по http1 с потерей лишних 2с на загрузку.

        • Блокировка основного потока приложения аж на 6с, из которых почти 3с - исполнение скриптов.

        • Создание избыточного DOM в 1к элементов для нескольких десятков ссылок и картинок.


        1. dlc
          17.08.2023 08:18

          Я как-то видел на Хабр.Карьере вакансию СТО с описанием в стиле "нужно писать весь код самому, а существующий СТО идёт пилить новый проект". Так что кто знает, кто там скрывается за шильдиком :)


  1. Safort
    17.08.2023 08:18
    +1

    Статья норм, но обратил внимание на один момент: вы используете ts-prune, который уже только на поддержке. В ридми проекта автор сам рекомендует юзать https://github.com/webpro/knip, ибо ts-prune у него нет сил больше развивать.


    1. andrey_stepanov1 Автор
      17.08.2023 08:18
      +1

      Спасибо за наводку!


  1. Ekernik
    17.08.2023 08:18

    Если я правильно понимаю документацию Next'a, то начиная с версии 13.0, swcMinify используется по-умолчанию.

    Следовательно можно смело избавиться от следующих строк.

    // next.config.js 
    module.exports = {
        swcMinify: true,
    }


  1. dagot32167
    17.08.2023 08:18

    Спасибо за статью.
    Но все же спрошу: "Что бы что?"
    немного уточнений:
    - главная страница, обычно, как разводящая, с акциями и предложениями. Не посадочная, туда трафик приходит в меньшей мере, чем на страницу каталога или товара (да, я понимаю, что оптимизация на примере главной, в рамках единого развертываемого модуля могло оптимизировать и остальные страницы, но тут от вас нет контекста по архитектуре, потому предполагаю, что все же микрофронт).
    - правки, которые вы выполнили, в большей мере влияют на первый вход пользователя на сайт. далее у него кешируется большинство статических ресурсов и они в меньшей мере влияют на его путь в отличии от бека (про оптимизацию которого нет в статье ни слова)

    т.е. вот вы улучшили перформанс главной страницы, как это повлияло на вашего пользователя? На какие бизнесовые метрики повлияло изменение технических? Есть ли корреляция?


  1. muturgan
    17.08.2023 08:18

    По ходу чтения статьи становится понятно, что вы имеете ввиду. Но из заголовка не понятно что за производительность такая. В моём понимании размер бандла или скорость загрузки страницы это всё же не синонимы производительности. Например если страничка весом 10кб выжирает всю оперативку или блокирует event loop она производительная или нет?