Как в 1.5 раза повысить производительность фронтенда высоконагруженного интернет-магазина на Next.j…
- пятница, 18 августа 2023 г. в 00:00:14
Приветствую! Меня зовут Андрей Степанов, я CTO во fuse8. Мне интересно знакомиться с опытом коллег по цеху и делиться своим. В сфере я уже больше 20 лет. В этой статье – небольшое погружение в задачу по повышению производительности крупного сайта, много полезных ссылок и инструментов, которые вы сможете использовать для своих проектов.
Один из наших проектов – сайт крупной компании по продаже автозапчастей и комплектующих. Это интернет-магазин, аудитория которого насчитывает около 500 тысяч уникальных пользователей в месяц. Оптимизировать нужно было главную страницу этого интернет-магазина.
На сайте, на которым велась работа, используется APM сервис elastic – система сбора метрик, согласно которым можно отследить производительность ресурса. Так как сайт долгое время не оптимизировался, а только дополнялся новыми фичами, показатели стали падать.
Не все показатели пока удалось вытянуть в зеленую зону, но улучшение для многих из них получилось кратным. Пока дальнейшие работы планируются, расскажем, какие шаги предприняли для текущего результата.
Для начала используем сервис, который позволяет быстро найти мертвые экспорты или код в проекте.
Пример команды для package.json:
"deadcode-check": "npx --yes ts-"deadcode-check": "npx --yes ts-prune -s \"pages/[**/]?
(_app|_document|_error|index)|store/(index|sagas)|styles/global\""
Такой результат получаем:
После отработки в консоле будет список проблемных мест. По ним проходимся вручную и удаляем мертвые сегменты.
Есть и другой пакет для оптимизации. С его помощью получаем список неиспользуемых npm пакетов. Затем вручную проходимся по списку и удаляем все лишнее, тем самым уменьшая вес проекта и наводя порядок.
Depcheck нужно использовать несколько раз, потому как после каждого прогона и удаления ненужных библиотек могут появится новые, которые становятся рудиментарными после первого прогона. Например, удаленная в первой очереди библиотека использовала другую, которая уже на втором прогоне определится как мертвая зависимость.
Используем плагин, который анализирует проект и показывает список дублей с разными версиями.
Вот пример результата его работы на примере проекта сортировки:
Однако от видов выдачи результатов может зависеть алгоритм дальнейших действий.
Например, вот такая выдача:
Здесь явно понимаем, что нужно обновить пакеты. Обновив зависимости, можно будет уменьшить вес бандла.
Другой пример:
В этом случае также требуются обновления. Если не понятно, что и где обновлять, в настройках вебпака добавляем alias, который подскажет сборщику, какую именно версию обновить и откуда ее взять. На примере redux:
Картинки на сайте до оптимизации не масштабировались в зависимости от размеров экрана. Мы поменяли все картинки на next image. Затем поменяли приоритеты загрузки. Те, что первыми попадают во viewport, должны загружаться с высоким приоритетом и без lazyloading – то есть максимально быстро. Это влияет на скорость отрисовки сайта.
Используем для всех картинок модуль next/image.
Добавляем современные форматы изображений вместо jpg и png. Получаем лучшее сжатие без потери качества и более быструю загрузку.
images: {
formats: ['image/avif', 'image/webp'],
}
Приоритезируем загрузку картинок above the fold (тех, что находятся в viewport при первоначальной загрузке страницы) - свойство Priority.
Для векторных картинок выставляем свойство unoptimized.
<Image
scr="/static/icons/mainPage/qualityControl.svg"
width={40}
height={40}
unoptimized
priority
alt="Quality control"
/>
Выяснилось, что, в коде на каждой странице много дубликатов. Элементы, которые должны один раз загрузиться, закэшироваться в браузере и использоваться всеми страницами, были вставлены в код каждой из страниц и потребляли ресурсы при загрузке снова и снова.
Дубликаты элементов в коде появились из-за отмены дефолтного разбиения кода на чанки в 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"
}
Если работа на проекте ведется регулярно, сопровождаясь частыми релизами, стоит подумать о настройке мониторингов производительности после каждого релиза. Так, после внедрения новых фич, можно будет сразу отрабатывать проседания показателей. Это гораздо удобнее, чем дожидаться критического падения производительности и оптимизировать все целиком.
А чтобы в целом держать руку на пульсе, советуем чек-лист по улучшению производительности страниц от Виталия Фридмана. Чек-лист меняется каждый год, поэтому советы и подходы в нем получаются максимально актуальными и пригодными для использования в проектах.
За подготовку и содействие в составлении материала выражаю большую благодарность фронтенд-разработчику Сергею Пестову.