company_banner

Как размер кода зависит от минификатора, сборщика и языка. Неожиданное обновление webpack

    Меня зовут Илья Гольдфарб, я разработчик интерфейсов Яндекса. Мне интересно следить за тем, как развиваются инструменты для сборки фронтенда, поэтому я стараюсь изучать изменения в каждом релизе популярных решений.

    В преддверии выхода пятой версии 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
    Итог: UglifyJS сжимает лучше на 3,5% (2,5% gzip).

    Что быстрее загружается: сжатая версия 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
    Итог: версия, сжатая Babel с компиляцией тайпскриптом под esnext, весит на 97 238 байт (8,2%) меньше. Так неожиданно много получилось, потому что ангуляр написан на TypeScript, а Vue и React на JavaScript Terser, как и Uglify, при сборке вебпаком не может вырезать неиспользуемый кусок кода, поставляемый из ангуляра тайпскриптом. Это баг компиляции данного примера. В сборке на другом проекте его может не быть, и разница будет гораздо меньше.

    Также видно, что объём 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
    Итог: Uglify с пятикратной минификацией меньше Uglify по умолчанию на 3,7% (3,4% gzip). Поэтому необходимо всегда докручивать настройки сжатия. Кстати, пятикратная минификация не означает, что сборка будет идти в пять раз дольше. Например, в данном тестовом проекте однократная минификация занимает 18 секунд, пятикратная — 38, а десятикратная — 49. Рекомендую опытным путём найти для своего проекта идеальное значение, после которого минификация остановится и код изменяться не будет. Обычно оно от 5 до 10. Также есть куча других опций: comments: false вырезает все комментарии о лицензиях (хотя тут юридический вопрос), а hoist_funs: true группирует функции в одном месте, что позволяет лучше оптимизировать var’ы. В идеале надо пробежаться по всем настройкам.

    Кто выдаёт сборку меньшего размера, 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
    Итог: результат от Rollup и Uglify на 5,6% (3,6% gzip) меньше.

    Так получилось по нескольким причинам:

    1. Вебпак содержит костыли для пограничных случаев. Например, этот код оборачивает каждый вызов функции из другого модуля в Object(). Это сделано, чтобы предотвратить перенос контекста для модулей без use strict в модули с use strict. Хорошо написанным проектам без сторонних зависимостей обёртка не нужна, но иногда в сборке участвует не только хорошо написанный код. И в этом плане webpack выглядит надёжнее. Роллап, в свою очередь, считает, что все модули — ES6 модули, а они всегда выполняются в use strict, так что этой проблемы для него просто не существует.

    Важный вопрос — как подобные костыли из вебпака влияют на производительность. Представим, что мы написали идеальный код, которому не нужны дополнительные обёртки, но всё равно каждый вызов функций будет проходит через них. Это добавляет небольшой оверхед при исполнении: примерно одну сотую микросекунды на каждый вызов функции в Chromium (одну десятую в Firefox).

    2. В вебпаке маленький по размеру бутстрап, управляющий инициализацией и загрузкой модулей. Rollup не использует обёртки, а просто скидывает в единую область видимости код всех модулей. У вебпака есть похожая оптимизация, но она работает не со всеми модулями.

    Итоги исследования


    Я надеюсь, что многие, прочитав статью, проверят свои системы сборки и убедятся, что применяют все возможные приёмы для наилучшего сжатия. Это быстро и несложно.

    Во-первых, правильно настройте связку TypeScript и Babel. Пусть каждый компонент сборки занимается своим делом: один проверяет типы, а второй отвечает за конвертацию под устаревающие стандарты.

    Во-вторых, при использовании ES5 можно сменить минификатор обратно на UglifyJS, но надо помнить, что он уже не поддерживается.

    В-третьих, для сборки предпочтительнее выбирать Rollup. Правда, не во всех случаях это возможно из-за отсутствия некоторых плагинов. После сборки не забывайте проверить работоспособность функциональными тестами. Если у вас их нет — самое время начать их писать.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Чем вы собираете свои проекты

    Яндекс
    575,00
    Как мы делаем Яндекс
    Поделиться публикацией

    Комментарии 10

      +1
      Мне казалось, что ES6 должен сокращать лучше, чем ES5. Данные статьи показывают, что это не всегда так.
        0
        Вы уверены что такое сравнение корректно?
        Тут просто сравнивается как сжимаются библиотеки, а не код приложения.
          0
          Код у каждого проекта свой и результат может отличаться вплоть до противоположного. Но в целом сравнение корректно
          +1

          Для ангуляра есть еще вариант ngc + closure compiler, причем если в коде не будет левых зависимостей можно включить advanced оптимизации. Интересно какой будет результат.

            0
            примерно одну сотую наносекунды на каждый вызов функции в Chromium (одну десятую в Firefox).

            Может быть одну сотую микросекунды? А то напоминает научную фантастику.

              0

              Вы правы, поправил

              0
              > Чем вы собираете свои проекты
              parcel / brunch
                0

                Следует еще заметить, что Webpack досрочно переехал на Tesrser под давлением общественности, которая была недовольна багами минификации React. uglify-es неправильно инлайнил функции, terser этой проблемы не имеет.

                  0

                  И еще вопрос к автору статьи: а есть ли в Яндексе сервисы, которые используют нетранспиленный ES6 в продакшене? И если есть, как там делается фоллбек для старых браузеров?

                    0
                    у меня с Tesrser была/есть в проектах на vue абсолютна противоположна по сравнению с uglify

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое