В преддверии выхода пятой версии webpack я хочу рассказать о его, казалось бы, минорном релизе 4.26.0 от 19 ноября 2018 года, где неожиданно и без объявления войны изменилась версия минификатора по умолчанию. Раньше это был пакет UglifyJS, теперь же используется Terser, форк UglifyES — ветки UglifyJS, которая может сжимать и ES5, и ES6 код. Terser появился, когда основной майнтейнер отказался поддерживать и развивать UglifyES. Впрочем, UglifyJS тоже прекратил свое развитие с августа 2018 года, когда был выпущен последний релиз. В новом форке исправили некоторые баги и немного отрефакторили код.
API этих минификаторов совместимый, но результат сжатия они выдают разный. Обычно изменения подобного уровня происходят лишь в мажорных, а не минорных обновлениях. Из-за этого многие разработчики могут не обратить внимания на нововведение. Конечно, в большинстве случаев всё будет работать, но никто не хочет стать тем, кто на продакшне своего проекта получит баги из-за системы сборки и минификации.
Вся эта история подвигла меня провести маленькое личное исследование сжатия. Вот вопросы, которые я задал:
- Что лучше сжимает ES5, Terser или UglifyJS?
- Что быстрее загружается: сжатая версия ES5 от Terser или от UglifyJS?
- Какая версия весит больше: ES5 или ES6? И как на это влияет TypeScript?
- Большая ли разница между настройками по умолчанию и ручной настройкой?
- А если не webpack? Кто выдаёт сборку меньшего размера, Rollup или webpack?
Для исследования я сделал небольшое приложение на React 16, которое рендерит приложение на Vue 2, которое рендерит приложение на Angular 7, в котором есть целая одна кнопка.
Итого вышло 3 529 695 байт неминифицированного кода (720 393 байта gzip).
Что лучше сжимает ES5, Terser или UglifyJS?
Я взял последний доступный UglifyJS и идущий вместе с вебпаком Terser с опцией ES5 и использовал одинаковые настройки сжатия.
Размер в байтах |
Размер в байтах (gzip) |
|
UglifyJS |
1 050 376 |
285 290 |
Terser |
1 089 282 |
292 678 |
Что быстрее загружается: сжатая версия ES5 от Terser или от UglifyJS?
Я измерял производительность с помощью стандартных DevTools Яндекс.Браузера. Загрузил страницу 12 раз и взял значение Scripting (время исполнения скрипта), отбросив первые три измерения.
UglifyJS — 221 мс (погрешность 2,8%).
Terser — 226 мс (погрешность 2,7%).
Итог: значения слишком малы для такой погрешности, можно считать их одинаковыми. Также делаем вывод, что этот метод не подходит для измерения времени загрузки.
Я не стал измерять и сравнивать скорость работы кода, поскольку разный код работает по-разному. Разработчики каждого проекта должны самостоятельно исследовать этот вопрос.
Какая версия весит больше: ES6 или ES5? И как на это влияет TypeScript?
Чтобы сравнить две версии и ориентироваться исключительно на технологии, я взял плагины Babel и сделал четыре сборки:
- ES5: все плагины, отмеченные как es2016, + плагин для Object.assign + плагины для поздних версий + экспериментальные плагины, target в tsconfig установлен в ES5;
- ES5 (ts esnext): все плагины, отмеченные как es2016, + плагин для Object.assign + все плагины для поздних версий + экспериментальные плагины, target в tsconfig установлен в esnext;
- ES6: только плагины для es2017 и поздних версий + экспериментальные плагины, target в tsconfig установлен в ES6;
- ES6 (ts esnext): только плагины для es2017 и поздних версий + экспериментальные плагины, target в tsconfig установлен в esnext.
Размер в байтах |
Размер в байтах (gzip) |
|
ES5 |
1 186 520 |
322 071 |
ES5 (ts esnext) |
1 089 282 |
292 678 |
ES6 |
1 087 220 |
292 232 |
ES6 (ts esnext) |
1 087 220 |
292 232 |
Также видно, что объём ES6 кода меньше ES5 всего на 2062 байта. На пет-проекте я получил совершенно другой результат: ES6 код на 3–6% больше, чем ES5. Это объясняется несколькими факторами, из них два основных:
1. Хелпер Babel для наследования классов вставляется один раз и потом стоит четыре байта (e(a,b)), а в ES6 используется нативное наследование ценой 15 байт (class a extends b).
2. Метод объявления переменных. В ES5 это var’ы, и они отлично сжимаются. А вот в ES6 это let и const, которые сохраняют порядок инициализации и между собой не объединяются.
Небезопасная агрессивная минификация вроде принудительных стрелочных функций или использования настройки loose поможет снизить размер ES6 кода. Будьте осторожны и учитывайте тонкости. Например, в Firefox стрелочные функции в четыре раза медленнее, чем обычные, а вот в Chromium нет никакой разницы.
Поэтому невозможно однозначно ответить на вопрос: результат сильно зависит от кода и целевой среды выполнения.
Большая ли разница между настройками по умолчанию и ручной настройкой?
Сравним, можно ли получить меньший размер файла, если немного подкрутить настройки. Например, укажем, что минификацию надо повторить пять раз. По умолчанию она проходит только один раз.
Размер в байтах |
Размер в байтах (gzip) |
|
Terser (по умолчанию) ES5 |
1 097 141 |
294 306 |
Terser (passes 5) ES5 |
1 089 312 |
292 408 |
Uglify (по умолчанию) ES5 |
1 091 350 |
294 845 |
Uglify (passes 5) ES5 |
1 050 363 |
284 618 |
Кто выдаёт сборку меньшего размера, Rollup или webpack?
Rollup — альтернативный сборщик со встроенным механизмом tree shaking. Для теста я сделал сборку на Rollup 0.67.4 с такими же настройками, как у вебпака.
Размер в байтах |
Размер в байтах (gzip) |
|
Rollup ES5 (Uglify) |
990 497 |
274 105 |
Rollup ES5 (Terser) |
995 318 |
272 532 |
webpack ES5 (Uglify) |
1 050 363 |
284 618 |
webpack ES5 (Terser) |
1 089 312 |
292 408 |
Так получилось по нескольким причинам:
1. Вебпак содержит костыли для пограничных случаев. Например, этот код оборачивает каждый вызов функции из другого модуля в Object(). Это сделано, чтобы предотвратить перенос контекста для модулей без use strict в модули с use strict. Хорошо написанным проектам без сторонних зависимостей обёртка не нужна, но иногда в сборке участвует не только хорошо написанный код. И в этом плане webpack выглядит надёжнее. Роллап, в свою очередь, считает, что все модули — ES6 модули, а они всегда выполняются в use strict, так что этой проблемы для него просто не существует.
Важный вопрос — как подобные костыли из вебпака влияют на производительность. Представим, что мы написали идеальный код, которому не нужны дополнительные обёртки, но всё равно каждый вызов функций будет проходит через них. Это добавляет небольшой оверхед при исполнении: примерно одну сотую наносекунды на каждый вызов функции в Chromium (одну десятую в Firefox).
2. В вебпаке маленький по размеру бутстрап, управляющий инициализацией и загрузкой модулей. Rollup не использует обёртки, а просто скидывает в единую область видимости код всех модулей. У вебпака есть похожая оптимизация, но она работает не со всеми модулями.
Итоги исследования
Я надеюсь, что многие, прочитав статью, проверят свои системы сборки и убедятся, что применяют все возможные приёмы для наилучшего сжатия. Это быстро и несложно.
Во-первых, правильно настройте связку TypeScript и Babel. Пусть каждый компонент сборки занимается своим делом: один проверяет типы, а второй отвечает за конвертацию под устаревающие стандарты.
Во-вторых, при использовании ES5 можно сменить минификатор обратно на UglifyJS, но надо помнить, что он уже не поддерживается.
В-третьих, для сборки предпочтительнее выбирать Rollup. Правда, не во всех случаях это возможно из-за отсутствия некоторых плагинов. После сборки не забывайте проверить работоспособность функциональными тестами. Если у вас их нет — самое время начать их писать.
Комментариев нет:
Отправить комментарий