Бюджеты производительности для JavaScript: обеспечение соблюдения лимитов пакетов в CI
Узнайте, почему стоимость использования JavaScript значительно превышает время его загрузки, как определить реалистичный бюджет производительности и как заставить webpack отклонять сборки, превышающие этот бюджет.
Источники: The Cost of JavaScript — Performance budgets and why your bundle size is driving users away
Страница, которая работает мгновенно на ноутбуке разработчика, может зависать на несколько секунд на телефоне реального клиента. Причина этого редко кроется только в скорости сети: она связана с работой процессора, необходимой для обработки каждого килобайта JavaScript при его загрузке на более слабое устройство. В этом руководстве объясняется, откуда берутся эти затраты, как превратить их в четко определенный бюджет производительности и как настроить процесс сборки так, чтобы он отклонял изменения, нарушающие этот бюджет, еще до того, как они попадут в продакшн.
Разница между компьютером разработчика и устройством пользователя
Большинство команд, занимающихся фронтендом, создают код и тестируют его на оборудовании, которое сильно не отражает реальные условия использования: современных ноутбуках, с проводными или оптоволоконными соединениями и сервере разработки, работающем на localhost. В таких условиях практически любой пакет кода кажется быстрым, поэтому размер пакетов часто рассматривается как что-то, что можно улучшить позже, а не как ограничение при проектировании.
Настоящий трафик выглядит иначе. Значительная часть посетителей попадает с смартфонами Android среднего класса, нестабильным 4G-соединением или перегруженными общественными сетями Wi-Fi. Представьте панель управления, требующую большого объема данных и мгновенно реагирующую в локальном браузере, но при тестировании на старых iPhone интерактивные элементы управления начинают работать только через примерно три секунды. В коде нет никаких ошибок; устройству просто требуется гораздо больше времени для обработки того же скрипта.
Такая задержка имеет финансовые последствия. Дополнительный, неоптимизированный JavaScript постепенно откладывает момент, когда пользователь может взаимодействовать с интерфейсом, и это особенно ощутимо в критически важных процессах, таких как вход в систему или добавление товара в корзину. Когда эти действия кажутся медленными, люди отказываются от них, поэтому чрезмерно большой размер пакета данных может незаметно отпугнуть пользователей.
Почему JavaScript стоит дороже, чем время его загрузки
В течение многих лет основным ограничением была задержка в сети, и работа над производительностью сосредотачивалась на уменьшении размера изображений и улучшении способов передачи данных, таких как HTTP/2. Эти проблемы по-прежнему важны, но для JavaScript сегодня более серьезной проблемой является то, что происходит после прибытия данных.
Скрипт недоступен сразу после завершения загрузки. Браузер должен его просмотреть, скомпилировать и запустить, причем большая часть этих операций выполняется на главном потоке, том же потоке, который отвечает за обработку нажатий, кликов и отрисовку. Вполне возможно, что пакет размером 400 КБ будет загружен быстро, но страница всё равно будет казаться медленной, поскольку устройство занято обработкой кода вместо реагирования на ввод пользователя.
На смартфонах среднего класса такой цикл парсинга-компиляции-выполнения может существенно ухудшить показатель Interaction to Next Paint (INP) — метрику, отражающую скорость реакции страницы на действия пользователя. Этот показатель напрямую влияет на оценки Core Web Vitals, причем низкие значения этих показателей обычно связаны с более низкой удержанностью пользователей и худшими бизнес-результатами. Поэтому любые серьезные усилия по улучшению этих показателей должны включать сокращение объема JavaScript-кода, который мобильные устройства должны парсить и выполнять, а не только уменьшение размера загружаемых файлов.
Что такое бюджет производительности и как его установить
Бюджет производительности JavaScript — это чётко определённый лимит на объём скрипта, который может включаться в страницу, маршрут или чанк. Технически это просто число, но его реальная ценность заключается в организационном аспекте: это соглашение, к которому присоединяется вся команда. Вместо того чтобы обнаруживать сбои после выпуска и спешно их исправлять, бюджет выступает в роли ограничения, которое не позволяет им быть включёнными с самого начала.
Практический совет: бюджет превращает вопросы производительности из чего-то, на что нужно реагировать, в ограничения, с которыми необходимо работать при проектировании, так же как и с требованиями к доступности или безопасности.
Начало установки таких ограничений связано с определением конкретных, измеримых показателей. Широко распространенной целью является поддержание размера JavaScript-кода критического пути на любой странице на уровне примерно 170 КБ в сжатом виде — этот показатель выбран так, чтобы средний мобильный устройство могло быстро достичь состояния интерактивности. Вам следует проверять такие цифры с учетом особенностей вашей аудитории, но принцип остается прежним: как только установлено ограничение, каждая новая зависимость и функция должны с самого начала обосновывать свой объем.
Несколько практических рекомендаций помогают легче справляться с бюджетом:
- Устанавливайте бюджет на каждый маршрут или точку входа, а не только для всего приложения, чтобы тяжелый административный раздел не съедал лимит ресурсов для главной страницы.
- Измеряйте размер файлов каждый раз одинаковым способом (в сжатом и несжатом виде), чтобы цифры оставались сопоставимыми между разными версиями приложения.
- Пересматривайте установленные ограничения при изменении состава аудитории или устройств, вместо того чтобы молча повышать их при каждой неудачной версии приложения.
Автоматизация проверки в процессе CI/CD
Бюджеты, от которых зависит то, что кто-то вспомнит их проверить, долго не существуют. Приложена новая функция, рецензент сосредотачивается на логике кода, никто не проверяет отчет о составе пакета, и в результате появляются регрессии. Единственный эффективный подход — встроить проверку непосредственно в пайплайн, чтобы каждое изменение автоматически анализировалось.
Примечание: благодаря выполнению проверки в рамках CI каждый запрос к коду проверяется с учетом бюджета, и никому не нужно помнить о ее выполнении.
Инструменты сборки, такие как Webpack, могут сделать это за вас: настройте их так, чтобы они рассматривали слишком большие размеры вывода как сбой сборки, а не как предупреждение. Тогда обратная связь поступает сразу же вместе с изменением, которое её вызвало. Если в pull request добавляется библиотека, которая генерирует фрагмент данных сверх установленного лимита, процесс сборки становится некорректным, и автору необходимо устранить проблему перед слиянием кода.
В приведённой ниже конфигурации используется опция performance от webpack. Установка значения hints в 'error' превращает предупреждения о размере в ошибки, из-за которых сборка не будет завершена. Параметр maxAssetSize ограничивает размер отдельных генерируемых файлов (здесь — 250 КиБ), а maxEntrypointSize — общий размер загружаемых ресурсов для одной точки входа (здесь — 500 КиБ); оба значения указываются в байтах. Функция assetFilter определяет, какие файлы вообще будут обрабатываться; в данном случае исключаются карты источника и JSON-файлы, чтобы избежать ложных сигналов.
module.exports = {
// ... other webpack configuration ...
performance: {
hints: 'error',
// Emit an error whenever a bundle exceeds the size limits
maxAssetSize: 250 * 1024,
// Maximum size in bytes for any generated asset (e.g., 250 KiB)
maxEntrypointSize: 500 * 1024,
// Maximum size in bytes for an entrypoint (e.g., 500 KiB)
assetFilter: function(assetFilename) {
// Optionally exclude certain assets (e.g., source maps) from performance hints
return !/\.(map|json)$/.test(assetFilename);
}
}
};
Имейте в виду, что webpack сравнивает эти лимиты с размерами генерируемых файлов, которые обычно не являются размерами в сжатом виде, загружаемыми пользователями. Если ваш лимит задан в сжатых килобайтах, преобразуйте его в размеры в несжатом виде для этой проверки или сочетайте её с отдельным шагом измерения размера вывода в формате gzip или Brotli.
Стратегии, позволяющие держать размеры пакетов в рамках лимита
Лимит и автоматическая проверка сообщают вам, когда вы превышаете его; для соблюдения лимита требуется тщательная работа инженеров. Самые эффективные методы хорошо известны и дают наилучшие результаты при совместном применении.
Разделение кода по маршрутам и функциям
Разделение кода позволяет преобразовать один монолитный пакет в более мелкие части, которые загружаются по мере необходимости. Частым примером является тяжелая панель управления для администраторов в корпоративном приложении: благодаря использованию динамических операторов import() ее код загружается только тогда, когда администратор действительно переходит по соответствующему маршруту. Обычные пользователи его никогда не загружают, что позволяет уменьшить размер начального пакета на сотни килобайт.
Используйте функцию удаления ненужного кода
Функция удаления ненужного кода удаляет элементы, которые никогда не используются, так что неработающие части собственных модулей и библиотек не попадают в итоговый результат. Она наилучшим образом работает с синтаксисом ES-модулей и зависимостями, опубликованными в формате, поддерживающем удаление ненужного кода; поэтому перед использованием того или иного пакета стоит проверить, как он скомпилирован.
Осторожно: использование крупной библиотеки только для одной вспомогательной функции — одна из наиболее распространенных причин увеличения размера приложения. Ищите более легкую альтернативу или импортируйте только ту функцию, которая вам действительно нужна.
Откладывайте выполнение того, что не требуется на первом экране
Разгрузочная загрузка компонентов, которые не критичны для первоначального отображения, таких как диалоговые окна, элементы, находящиеся ниже линии прокрутки, или редко используемые панели, откладывает их обработку и выполнение до момента, когда они действительно понадобятся. В сочетании с разделением кода и оптимизацией это позволяет сохранять свободной основную поток выполнения в первые секунды, когда пользователи наиболее склонны взаимодействовать с приложением.
Заключение
- Дорогостоящими этапами работы JavaScript в мобильных устройствах являются его парсинг, компиляция и выполнение в основном потоке, что напрямую влияет на показатель INP и другие ключевые метрики Core Web Vitals.
performance.hints: 'error' в webpack, позволяет выявлять регрессии в pull request, приводящие к его нарушению.