«Наши инструменты сборки веб-приложений от 10 до 100 раз медленнее, чем они могут быть» – считает Эван Уоллес, сооснователь Figma. По его мнению, прямо сейчас, а не в будущем, можно собирать фронтенд в 10–100 раз быстрее. Рассмотрим, как этого добиться, и причём тут webpack.
Поможет нам в этом Евгений Кувшинов, фронтендер и тренер по инженерным практикам с двенадцатилетним опытом в продуктовой разработке. Он расскажет про свой опыт работы с webpack и поможет поставить запятую в заголовке статьи.
Что не так с webpack?
Статистика NPM подтверждает безоговорочное лидерство Webpack среди систем сборки. Но значит ли это, что самый популярный продукт – лучший на рынке? У webpack развитая экосистема, решения на все случаи жизни. Но есть один нюанс. Сборка пустого проекта create-react-app с webpack может занимать 20–30 секунд на современном ноутбуке. Что же происходит на большом проекте?
Скорость сборки – точка А
Евгений рассказал, что в начале 2021 года они использовали webpack для сборки крупного одностраничного приложения с пятилетней историей разработки. На сборку требовалось больше трёх минут! Причём и на пересборке webpack не показывал себя гоночным болидом: после любого изменения приходилось ждать около 10 секунд.
Где болит от медленной сборки?
Тормозит на компьютере разработчика
Представьте, солнечное летнее утро, вы спешите на встречу с командой показать прогресс по задаче. Открываете ноутбук, все смотрят на экран, но еще 3 минуты консоль webpack-а показывает надпись «wait until bundle finished». Наконец приложение запустилось. Нужно поправить текст на кнопке, и приходится ждать еще секунд 10. Через час работы ноутбук, простоявший всю ночь на зарядке, выдает надпись «низкий заряд батареи» и экран гаснет.
В ежедневной работе фронтенд-разработчики зачастую вносят изменения в код через IDE и переключаются в окно браузера, чтобы посмотреть результат. Таких переключений могут быть сотни в день, а задержка при пересборке отнимает время.
Таким образом, скорость сборки напрямую влияет на время выполнения задач и удобство процесса разработки – Developer Experience. Это значит, что от уменьшения времени сборки выигрывают все: и организация, и разработчики.
Тормозит на CI-сервере при интеграции
Помимо локальной сборки есть ещё и сборка в пайплайне на сервере системы непрерывной интеграции – CI. Локально можно как-то исхитриться с кэшем или ленивой сборкой, а на CI надо собирать всё с нуля.
Если верить отчётам State of DevOps, для успеха компании важны вот эти метрики:
Lead time for changes;
Deployment frequency;
Time to restore.
Отчёты выпускаются с 2014 года, включают результаты опроса более 32 000 разработчиков и скореллированы с бизнес-результатами их компаний. Данные статистически доказаны, а методика подробно описана в книге “Ускоряйся! Наука DevOps”.
Скорость сборки напрямую влияет на все три метрики, что неудивительно: чем быстрее проходит сборка и тестирование на CI, тем быстрее код готов к релизу, а значит можно релизить чаще. Бывает, что в релизе обнаруживается баг и нужно срочно выпустить фикс. Чем быстрее он пройдёт пайплайн, тем быстрее работа будет восстановлена, time to restore уменьшается.
Самая интересная метрика, по мнению Евгения, lead time for changes. Это время от момента коммита изменений до того, как они оказываются на продакшене. Если вы захотите работать в режиме непрерывной интеграции и деплоить несколько небольших релизов в день, время прохождения CI пайплайна станет критичным. Например, пайплайн в 10 минут на 6 релизах в день будет занимать час рабочего времени.
Скорость сборки – точка Б
Сборка за 3 минуты Евгения не устраивала, поэтому поставили цель:
Прохождение всего пайплайна на CI за 4 минуты, вместе со сборкой, линтером, юнит и UI тестами. Текущий прогон занимает 8 минут из них сборка 3 минуты.
Чтобы достичь цели, сборка должна проходить меньше, чем за минуту. Желательно за 30 секунд. Как это сделать?
Прежде всего давайте разберёмся, как проходит процесс сборки веб-приложений, какие инструменты используются, и только ли webpack влияет на скорость.
Bundler, build tool, transpiler – кто есть кто?
Transpiler
Начнём с transpiler, он же source-to-source compiler. Популярный представитель этого класса инструментов – babel. На его сайте есть страничка с repl. На ней можно подать ему на вход исходный код и получить преобразованный код на выходе:
Транспайлеры используют для того, чтобы писать на TypeScript или использовать свежие фичи ECMAScript в браузерах, в которых они ещё не поддерживаются.
Транспайлеры используются в процессе сборки, но одного транспайлера недостаточно. Современные приложения состоят из множества файлов разных типов: скрипты, стили, картинки. Чтобы собрать все эти файлы в готовую поставку, нужен следующий инструмент – bundler.
Bundler
Bundler, бандлер или сборщик. На сцену выходит главный герой статьи – webpack. В его зоне ответственности 3 шага процесса сборки:
Построить граф всех зависимостей;
Загрузить и преобразовать зависимости;
Скомпоновать и сохранить преобразованные зависимости в виде готовых для загрузки в браузер статических ассетов.
На скриншоте с официального сайта webpack схематично изображен весь процесс. На втором шаге для преобразования кода подключают какой-нибудь transpiler, например, babel. Это популярная связка используемая во множестве компаний.
Build tool
Build tool – новый класс инструментов. Одним из первых его представителей стал vite:
На официальном сайте написано, что Vite состоит из двух частей: dev-сервер и команда для сборки проекта при помощи Rollup. Rollup – это бандлер, один из конкурентов webpack. Для сборки внешних зависимостей Vite использует дополнительный бандлер – ESBuild. Получается, что у Vite под капотом работают целых два бандлера.
Возникает вопрос: зачем так много? Дело в том, что Rollup – зрелый сборщик с широкими возможностями, которые покрывают множество сценариев использования. Проблема скорости сборки для Rollup также актуальна, как и для webpack. ESBuild – бандлер нового поколения. Он не так функционален, как webpack и rollup, но может выполнять сборку в 10–100 раз быстрее.
Кстати, создатель ESBuild – тот самый Эван Уоллес.
Бандлеры нового поколения
ESBuild не единственный представитель современных сборщиков, его основной конкурент – swc.
На официальном сайте SWC сказано, что это быстрый веб-компилятор, который может быть использован и как сборщик, и как транспайлер.
На сайте ESBuild написано, что это экстремально быстрый сборщик для веб-приложений. Правда сборку нужно включать специальной опцией bundle, а по умолчанию ESBuild работает только в режиме транспайлера.
Бандлеры нового поколения объединяет:
высокая скорость;
встроенный транспайлер с поддержкой современного ECMAscript, TypeScript и JSX из коробки;
встроенный механизм минификации.
Ускоряем сборку – попытка №1
Когда Евгений узнал про бандлеры нового поколения, то подумал: а что будет, если какой-нибудь из них использовать в роли транспайлера вместо babel? Это должно помочь за минимум усилий разогнать сборку! На npm нашелся готовый пакет esbuild-loader, его и попробовали подключить. В результате сборка прошла с неплохим результатом – около 20% ускорения. Но этого было недостаточно.
Как еще можно ускорить сборку с webpack?
На проекте Евгения за 6 лет webpack оброс конфигурацией в несколько сотен строк кода, а местами его настройка проникла и в сам код приложения. Замена бандлера целиком показалась непростой задачей, поэтому было решено выжать максимум из webpack. Пробовали:
Искать рецепты ускорения через конфигурацию, но заметных результатов не получили.
Накатывать все свежие обновления webpack и лоадеров – тоже без значительных успехов.
Делать ленивую сборку, то есть разделять код на чанки по страницам и собирать их в моменты обращения к странице из браузера. Такой подход делает первоначальную сборку быстрее, но когда разработчик открывает браузер и начинает переходить по страницам, ему всё равно приходиться ждать около минуты, чтобы подсобрать этот чанк. На CI такой приём вообще не помогает, так как там нужна полная сборка.
Использовать кэш. Вторая важная проблема программирования после именования переменных – инвалидация кэша. Кэширование иногда приводило к сборке неактуального кода с ошибками. Если объем изменений кода относительно кэшированной версии был велик, то сборка была еще медленнее, чем без кэша.
Как получить обещанное в 10–100 раз ускорение?
Замены babel на esbuild-loader и применения всевозможных твиков webpack оказалось недостаточно. Дело в том, что построение графа зависимостей при сборке тоже занимает время. Для максимального ускорения нужны:
Параллелизация, чтобы обрабатывать несколько файлов за раз.
Минимизация переходов из Native в JS. ESBuild и SWC написаны на нативных платформах. ESBuild на Go, SWC на Rust. Если использовать их в режиме сборки, вся работа будет проходить внутри одного системного процесса.
Остался только один выход – заменить бандлер полностью. Для переезда составили такой план:
Подготовка: отказ от специфики webpack + Babel.
Выбор бандлера: SWC или ESBuild.
Настройка сборки и замеры: скорость сборки, производительность приложения в lighthouse.
Перевод локальной сборки на новый бандлер.
Сбор обратной связи от других разработчиков.
Перевод продакшн-сборки на новый бандлер.
Подготовительный этап
В начале нужно было отказаться от специфики webpack, которая проникла в код приложения. У webpack есть нестандартный синтаксис импорта зависимостей. Например, он позволяет подключать нужный лоадер для файла прямо в строке импорта. Конкретно Евгений использовал ещё один нестандартный механизм – require.context
//…
import(/* webpackPreload^ true */’ChartingLibrary’);
require.context(‘../’, true, /\.stories\.js$/);
import { myFunction } from “exports-loader?exports=myFunction!./file.js»;
Вместо require.context перешли на более распространенный import c glob-паттерном: import(‘../**/*.js’).
Также пришлось отказаться от специфических babel-плагинов, потому что был JSX Condition Statements:
<If condition={ true }>
<span>IfBlock</span>
</If>
Этот плагин заменяет конструкцию на {true && <span>IfBlock</span>}.
Ещё у Webpack есть глобальные переменные, которые тоже пишутся прямо в коде. Например, у Евгения один разработчик использовал __Webpack_Public_Path__, чтобы подключить CDN в продакшене, а потом уволился. Все забыли про эту переменную и, когда первый раз выкатили сборку на новом бандлере, CDN не подключился, а продакшн упал. Пришлось откатываться.
В итоге всю специфику webpack и babel, кроме __Webpack_Public_Path__, из кода убрали. Подготовка была завершена, нужно было выбрать новый бандлер.
SWC или ES build?
Выбор начался с просмотра трендов npm:
Оба инструмента часто обновляются, имеют много звёздочек и примерно одинаковое количество issue – тренды не слишком помогли.
Решили выделить один день на тест SWC, а другой — на ESBuild.
С swc возникли сложности. При сборке приложения вылетали ошибки, которые непонятно было как устранять, и что делать дальше. На следующий день попробовали ESBuild и процесс пошёл веселее. Очень понравилась документация и как ESBuild формирует ошибки. За день сделали первую сборку! Правда, это была белая страница без стилей, но это было уже что-то.
3 шага к быстрой сборке на ESBuild
Для переезда нужно рабочее приложение. Поэтому первый шаг — полностью настроить ESBuild. Сложности были с тем, что в ESBuild нет встроенной поддержки CSS модулей и Stylus, которые были в нашем проекте. Пришлось писать свои плагины. На ESBuild это оказалось легко! Через неделю мы получили первую сборку рабочего приложения, увидев ускорение в 7.2 раза. Вместо 3-х минут с webpack – 25 секунд на ESBuild.
Но и этого было недостаточно.
Второй шаг — отказ от ненужного и устаревшего. Пристально посмотрели на Stylus, используемый для работы с CSS в проекте. Это мощный инструмент, но он почти не развивается и работает достаточно медленно. Оказалось, что процессинг Stylus занимает больше половины времени сборки. Договорившись с командой, перевели проект со Stylus на чистый CSS, и получили ускорение сборки в 18 раз.
Последним шагом стало кэширование переиспользуемых CSS модулей.
Итоговый результат: ускорение в 30 раз, то есть 2.5–3 секунды на сборку с нуля. При этом пересборка занимает 0.2–0.3 секунды.
Когда показывали сборку на ESBuild другим разработчикам, они не могли поверить: сделал изменения в коде, переключился в браузер, а уже всё обновилось! При том, что в ESBuild нет горячей перезагрузки модулей – HMR, и страница полностью обновляется после каждой пересборки.
Итоги
Хорошие
→ Сборка ускорилась в 30 раз;
→ Улучшились метрики в lighthouse за счёт перехода на формат ESM и большего количества чанков кода;
→ Ноутбуки разработчиков не шумят вентиляторами, а батареи хватает на долго.
Не очень хорошие
→ 3 недели подготовки и 3 недели настройки;
Переход стоит дорого, но в долгосрочной перспективе экономит много времени и сил.
→ Некоторые плагины придётся писать самостоятельно;
Экосистема ESBuild пока на слишком развита, готовых плагинов не хватает.
→ Трудно найти специалистов с опытом настройки ESBuild.
Если пойдёт что-то не так со сборкой, новичкам непросто с этим разобраться. Они будут говорить: «ребята, что-то не так с вашим ESBuild. Пожалуйста, почините».
Заключительная мысль
Если скорость сборки не самый важный для вас критерий, можно оставить webpack. С ростом приложения и команды ситуация может измениться. Скорость сборки будет всё больше влиять на эффективность разработки и бизнесовые метрики. Тогда webpack потребуется заменить. Я рекомендую не уходить глубоко в специфику webpack, чтобы оставаться гибким и иметь возможность в любой момент попробовать что-то другое.
Просто попробуйте ESBuild и swc, чтобы почувствовать эту невероятную разницу в скорости сборки и примите своё решение: где поставить запятую в заголовке.
Наконец, сложно обойти стороной Vite. У него простой старт: postcss, glob import и многое другое работает из коробки. Он даёт абстракцию над бандлером. Возможно, в какой-то момент автор полностью заменит Rollup на ESBuild, что сделает Vite значительно быстрее без вашего участия.
Если хочется взглянуть на альтернативу webpack с низким порогом входа, то Vite — хороший вариант. Если нужна сборка по хардкору и супербыстро, стоит смотреть в сторону SWC и ESBuild. Но будьте готовы к трудностям по дороге!
Комментарии (6)
gmtd
00.00.0000 00:00+1Я так понимаю, вы могли бы избавиться только от Stylus и может еще пару легаси штук, и уже бы были счастливы с текущим сборщиком
AlexanderY
00.00.0000 00:00Подскажите, как именно вы поняли, что работа со Stylus отнимает так много времени сборщика?
Хороший результат. У нас в проекте не так всё печально: на современном ноутбуке сборка занимает 40 секунд, а вот на CI несколько минут. Если бы получилось хотя бы в 5 раз ускориться, было бы круто. Но тоже большие конфиги webpack. Нахрапом не взять, думаю. Больше всего пугает неизвестное время на исследование.
flancer
00.00.0000 00:00Второй шаг — отказ от ненужного и устаревшего.
Можно уже не хейтить тех, кто пишет на ES6+ вместо TS или ещё рано?
faiwer
00.00.0000 00:00+2На моём опыте самое медленное звено — Typescript + EsLint. И заменить их нечем. "Аналоги" на rust/go работают молниеносно, но тому есть причина — они не делают 99% работы, т.е. type-checking-а и linting-а. А это именно то, что мне экономит больше всего времени и сил. Если в теории кто-то возьмётся и за это, то боюсь, что встанет другой вопрос — расхождения реального TS Language Server-а и rust/go решения.
Похоже, что пока что самое рабочее решение — это отделять сборку и type-checking. Т.е. условно сборка за 2.5сек, а потом вдогонку к ней прилетает секунд через 30 проверка типов и eslint.
Отдельная беда — дублирование. Тот же TS Language Server на машине разработчика запущен дважды/трижды: в сборке (1-2 раза) и в vsCode/IDE. Т.к. оный на больших проектах легко отъедает и 5 и 10 GiB… это печально.
DmitryKazakov8
00.00.0000 00:00+1На Хабре обсуждалось не раз предложение "сделать единое AST для TS, ESLint, Webpack и других сборщиков, переиспользовать в IDE", вроде вы тоже участвовали в обсуждениях. Но каждый раз приходим к тому, что есть разные лицензии, корпоративные политики (TS), ограничения операционных систем, разность механик работы разных инструментов. Я тоже хотел бы видеть "универсальный процесс, генерирующий универсальное дерево с адаптером ко всем языкам программирования", это был бы, наверное, лучший вклад в экосистему разработки за десятилетия, но остается только мечтать...
SWATOPLUS
Сборщики работали бы быстрее, если бы они работали с уже собранными пакетами. Если в пакету будет index.js и index.d.ts то сборка будет идти гораздо быстрее. Но во много пакетах, помимо кучи транзитивных зависимостей или и бардак с самим кодом. Там зачастую, просто хранят исходники, а бывает и юнит тесты. Про esbuild все и так знают. Но что насчёт пакетов? Почему никто не бьёт в колокол по этому поводу?