Приветствую! Меня зовут Андрей Степанов, я 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 – то есть максимально быстро. Это влияет на скорость отрисовки сайта.
Используем для всех картинок модуль 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"
/>
Code splitting
Выяснилось, что, в коде на каждой странице много дубликатов. Элементы, которые должны один раз загрузиться, закэшироваться в браузере и использоваться всеми страницами, были вставлены в код каждой из страниц и потребляли ресурсы при загрузке снова и снова.
Дубликаты элементов в коде появились из-за отмены дефолтного разбиения кода на чанки в 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)
nin-jin
17.08.2023 08:18+1Оно уже на проде? А то я вижу там на главной такую картину:
andrey_stepanov1 Автор
17.08.2023 08:18Да, в проде - но где не могу сказать по NDA :)
nin-jin
17.08.2023 08:18То есть следующие моменты вас не смутили при тестировании довольно простой главной страницы, я правильно понимаю?
30 скриптов, общим объёмом в пол мегабайта в сжатом виде.
Загрузка множества мелких файлов по http1 с потерей лишних 2с на загрузку.
Блокировка основного потока приложения аж на 6с, из которых почти 3с - исполнение скриптов.
Создание избыточного DOM в 1к элементов для нескольких десятков ссылок и картинок.
dlc
17.08.2023 08:18Я как-то видел на Хабр.Карьере вакансию СТО с описанием в стиле "нужно писать весь код самому, а существующий СТО идёт пилить новый проект". Так что кто знает, кто там скрывается за шильдиком :)
Safort
17.08.2023 08:18+1Статья норм, но обратил внимание на один момент: вы используете ts-prune, который уже только на поддержке. В ридми проекта автор сам рекомендует юзать https://github.com/webpro/knip, ибо ts-prune у него нет сил больше развивать.
Ekernik
17.08.2023 08:18Если я правильно понимаю документацию Next'a, то начиная с версии 13.0, swcMinify используется по-умолчанию.
Следовательно можно смело избавиться от следующих строк.// next.config.js module.exports = { swcMinify: true, }
dagot32167
17.08.2023 08:18Спасибо за статью.
Но все же спрошу: "Что бы что?"
немного уточнений:
- главная страница, обычно, как разводящая, с акциями и предложениями. Не посадочная, туда трафик приходит в меньшей мере, чем на страницу каталога или товара (да, я понимаю, что оптимизация на примере главной, в рамках единого развертываемого модуля могло оптимизировать и остальные страницы, но тут от вас нет контекста по архитектуре, потому предполагаю, что все же микрофронт).
- правки, которые вы выполнили, в большей мере влияют на первый вход пользователя на сайт. далее у него кешируется большинство статических ресурсов и они в меньшей мере влияют на его путь в отличии от бека (про оптимизацию которого нет в статье ни слова)
т.е. вот вы улучшили перформанс главной страницы, как это повлияло на вашего пользователя? На какие бизнесовые метрики повлияло изменение технических? Есть ли корреляция?
muturgan
17.08.2023 08:18По ходу чтения статьи становится понятно, что вы имеете ввиду. Но из заголовка не понятно что за производительность такая. В моём понимании размер бандла или скорость загрузки страницы это всё же не синонимы производительности. Например если страничка весом 10кб выжирает всю оперативку или блокирует event loop она производительная или нет?
Opaspap
1.1 сек многовато, если с кэшом. Но насколько я помню, эта хромовская тулза вообще плохо кэш учитывает, у меня приложение с загрузкой в 300мс медианы получало очень стремные скоры от маяка этого, просто потому что выкидывало service worker и его кэш :)
andrey_stepanov1 Автор
Webpagetest умеет проверять повторные загрузки страницы с кешем браузера, но его работу с service worker мы не проверяли.