[Перевод] JavaScript tree shaking, like a pro
Это перевод статьи об оптимизации и уменьшении размера бандла приложения. Она хороша тем, что тут описаны best practices, советы, которых стоит придерживаться, чтобы тришейкинг работал и выкидывал неиспользуемый код из сборки. Она будет полезной многим, потому что сейчас все используют системы сборки, в которых «из коробки» есть тришейкинг. Но чтобы он работал правильно, нужно придерживаться принципов, описанных ниже.
Тришейкинг становится основным приёмом, когда нужно уменьшить размер бандла и повысить производительность приложений на JS.
Как работает тришейкинг:
- Вы объявляете импорты и экспорты в каждом модуле.
- Сборщик (Webpack, Rollup или другой) во время сборки анализирует дерево зависимостей.
- Неиспользуемый код исключается из итогового бандла.
Файл с утилитами экспортирует две функции,
но используется только initializeName, formatName может быть удалена.
К сожалению, для правильной работы тришейкинга одной настройки сборщика недостаточно. Чтобы достичь лучшего результата, необходимо учесть множество деталей, а также удостовериться, что модули не были пропущены при оптимизации.
С чего начать?
Существует огромное количество руководств по настройке тришейкинга. Начать погружение в тему лучше с официальной документации Webpack.
Стоит упомянуть, что несколько лет назад я создал бойлерплейт с уже настроенной сборкой и тришейкингом. Так что если вам нужна отправная точка для проекта, мой репозиторий может стать хорошим примером krakenjs/grumbler.
В этой статье рассматривается работа с Webpack, Babel и Terser. Тем не менее, большинство представленных принципов будут работать вне зависимости от того, используете ли вы Webpack, Rollup или что-то ещё.
Используйте синтаксис ES6 для импортов и экспортов
Использование ES6 импортов и экспортов — первый и важнейший шаг к работающему тришейкингу.
Большинство других реализаций паттерна «модуль», включая commonjs и require.js, являются недетерминированными в процессе сборки. Эта особенность не позволяет сборщикам типа Webpack точно определить, что импортируется, что экспортируется и, как следствие, какой код может быть безболезненно удалён.
Варианты, возможные при использовании commonjs.
При использовании ES6 модулей возможности импорта и экспорта более ограничены:
- вы можете импортировать и экспортировать только на уровне модуля, а не внутри функции;
- имя модуля может быть только статичной строкой, не переменной;
- всё, что вы импортируете, обязательно должно быть где-то экспортировано.
ES6 модули имеют более простую семантику и правила использования.
Упрощённые правила позволяют сборщикам точно понимать, что было импортировано и экспортировано, и, как следствие, определять, какой код не используется вовсе.
Не разрешайте Babel транспилировать импорты и экспорты
Первая проблема, с которой вы можете столкнуться, используя Babel для транспиляции кода, — включённое по умолчанию преобразование ES6 модулей в commonjs. Это мешает сборщику оптимизировать код и выкидывать лишнее.
К счастью, в конфиге Babel существует простой способ отключить транспиляцию модулей.
После того как вы это сделаете, сборщик сможет взять транспиляцию импортов и экспортов на себя.
Делайте ваши экспорты атомарными
Webpack, как правило, оставляет экспорты нетронутыми в следующих случаях:
- экспортируется объект с большим количеством свойств и методов;
- экспортируется класс с большим количеством методов;
- экспорт объекта по умолчанию используется для экспорта множества разных функций.
Такие экспорты будут либо полностью включаться в бандл, либо полностью удаляться. Значит, в итоге у вас может получиться бандл, содержащий код, который никогда не будет использоваться.
Обе функции будут включены в бандл, даже если используется только одна.
И здесь класс будет целиком добавлен в сборку.
Старайтесь сохранять ваши экспорты настолько маленькими и простыми, насколько это возможно.
В итоговый бандл попадёт только та функция, которая будет использована.
Выполнение этого совета позволяет сборщику выкидывать больше кода благодаря тому, что теперь в процессе сборки можно отследить, какая из функций была импортирована и использована, а какая не была.
Этот совет также помогает писать код в более функциональном и направленном на переиспользование стиле, а также избегать использования классов там, где это не оправдано.
Если вам интересно функциональное программирование, обратите внимание на эту статью.
Избегайте побочных эффектов на уровне модуля
При написании модулей многие люди упускают важный, но очень коварный фактор — влияние побочных эффектов.
Webpack не понимает, что делает window.memoize, и поэтому не может выкинуть эту функцию.
Заметьте, в примере выше window.memoize
будет вызвана в момент импорта модуля.
Как это видит Webpack:
- окей, здесь создаётся и экспортируется чистая функция add — может быть я смогу удалить её, если она не будет использоваться позже;
- далее вызывается window.memoize, в которую передаётся add;
- я не знаю, что делает
window.memoize
, но я знаю, что она, возможно, вызовет add и создаст побочный эффект. - так что для сохранности я оставлю функцию add в бандле, даже если её больше никто не использует.
В реальности мы уверены, что window.memoize
— чистая функция, которая не создаёт никаких побочных эффектов и вызывает add, если кто-то использует memoizedAdd
.
Но Webpack этого не знает и для спокойствия добавляет функцию add в итоговый бандл.
Для честности: последние версии Webpack и Terser необычайно хорошо справляются с выявлением побочных эффектов.
Даём Webpack больше информации и получаем оптимизированный бандл.
Теперь сборщику хватит информации для анализа:
- здесь вызывается
memoize
на уровне модуля, это может быть чревато проблемами; - но функция
memoize
пришла из ES6 импорта, нужно взглянуть на функцию вutil.js
; - действительно, memoize выглядит как чистая функция, здесь нет побочных эффектов;
- если никто не использует функцию
add
, мы можем безопасно исключить её из итогового бандла.
Когда Webpack не получает достаточно информации для принятия решения, он пойдёт по безопасному пути и оставит функцию.
Используйте инструменты для выявления возможных проблем тришейкинга
Я нашёл два инструмента для выявления проблем.
Первый инструмент — module concatenation, плагин для Webpack, который позволяет добиться существенного прироста производительности. У него есть опция отладки. Стоит отметить, что факторы, предотвращающие конкатенацию и тришейкинг, одинаковые: например, побочные эффекты на уровне модуля. Воспринимайте предупреждения плагина серьёзно, так как любая проблема потенциально увеличивает размер бандла.
Второй — плагин для линтера https://www.npmjs.com/package/eslint-plugin-tree-shaking. Я ещё не интегрировал его в свой бойлерплейт, потому что он не поддерживал flow, когда я экспериментировал с ним. Однако, он довольно хорошо определял проблемы с тришейкингом.
Будьте осторожны с библиотеками
Старайтесь использовать оптимизированные для тришейкинга версии библиотек. Если вы импортируете большой бандл минимизированного кода, например jquery.min.js
, существует вероятность, что этот модуль не будет оптимизирован. Лучше поискать модуль, из которого можно импортировать атомарные функции, а для сборки и минификации использовать Webpack или Rollup.
Иногда вы можете импортировать всю библиотеку. Например, при использовании продакшен-билда React вам не нужно ничего выкидывать — всё, что в нём есть, уже оптимизировано.
Если вы используете библиотеку, экспортирующую отдельные функции, например lodash, попробуйте импортировать только нужные функции и обязательно удостоверьтесь, что остальные были исключены из итогового бандла.
Используйте флаги сборки
У плагина DefinePlugin для Webpack есть замечательная, но не самая известная фича — возможность влиять на то, какой код будет исключён в процессе сборки.
Если мы передадим __PRODUCTION__: true
в плагин, из итогового бандла будет исключён не только вызов функции validateOptions
, но и её определение.
Это упрощает создание разных бандлов для разработки и продакшена, а также помогает быть уверенным в том, что код, предназначенный для отладки, не попадёт в продакшен.
Запускайте сборку
На глаз очень трудно определить, как Webpack будет оптимизировать конкретный модуль.
Так что запускайте билд, проверяйте итоговый бандл и смотрите, что получается. Посмотрите JavaScript-код и убедитесь, что в нём не осталось ничего лишнего, что должно было быть выброшено тришейкингом.
Что-то ещё?
Если вы знаете другие полезные советы, напишите, пожалуйста, об этом в комментариях.