Если вы разработчик сайта, то наверняка пользуетесь каким-либо сборщиком (например, Webpack, Rollup или Parcel), который к тому же транспилирует ваш JavaScript-код с помощью Babel. Ну и, конечно же, вы наверняка используете разнообразные зависимости, чтобы сократить время разработки.
Обычно транспилировать код зависимостей не принято, ведь вроде как и без этого все прекрасно работает. Но времена меняются…
(В этой статье я делюсь своим опытом работы над open source проектами. Это и мое хобби, и работа в Cube, где мы создаем open source инструменты для создания приложений для работы с данными.)
Распространение ESM
До появления нативной поддержки ES-модулей в браузерах и в Node.js, npm-пакет мог содержать несколько вариантов кода:
- CommonJS-вариант без использования новых фишек JavaScript, таких как стрелочные функции. Такой код совместим с большинством версий Node.js и браузеров. Файл этого варианта указывают в поле
main
вpackage.json
. - module-вариант, использующий ES6-импорты и экспорты, но так же без использования новых фишек. Модули позволяют делать tree-shaking, то есть не включать в бандл неиспользуемый код. Файл этого варианта указывают в поле
module
вpackage.json
.
Конечно, не все npm-пакеты сделаны по такому принципу — все зависит от разработчика. Однако браузерные и универсальные библиотеки часто распространяются именно так.
Сейчас ES-модули нативно поддерживаются браузерами уже больше трёх лет, а в Node.js их поддержка появилась в версии 12.20, выпущенной в ноябре 2020 года. Разработчики библиотек стали включать в npm-пакеты еще один вариант кода — для сред, нативно поддерживающих ES-модули, или вовсе перешли на поддержку только ES-модулей.
Особенности ESM
Важно понимать, что нативные ES-модули — это не то же самое, что модули, использующие ES6-синтаксис импорта и экспорта:
То, как мы привыкли использовать
import
/export
, не будет нигде работать нативно — такой код предназначен для дальнейшей обработки бандлером или транспайлером.-
Нативные ESM не умеют резолвить расширения и index-файлы, поэтому их нужно явно указывать в пути:
// Неправильно: import _ from './utils' // Правильно: import _ from './utils/index.js'
Если пакет содержит ESM-код, то в
package.json
нужно явно указать тип пакета с помощью"type": "module"
.Для указания файла с ESM в
package.json
нужно использовать полеexports
.
Более подробно про особенности ESM можно прочитать в этой статье.
Поддержка JavaScript
Еще одна особенность ESM это то, что мы точно знаем, с каких версий браузеры и Node.js поддерживают ES-модули. Соответственно, мы точно знаем, какие фишки JavaScript мы можем использовать в коде.
Например, все браузеры с нативной поддержкой ES-модулей имеют поддержку стрелочных функций, а значит нам больше не нужно избегать их использования или настраивать Babel для их транспиляции в обычные функции. Разработчики библиотек пользуются этой особенностью и используют в ESM-коде все новые фишки JavaScript.
Транспиляция зависимостей
Но что делать, если ваш сайт должен работать в более старых браузерах? Или если какая-нибудь из зависимостей использует новые фишки JavaScript, не поддерживаемые актуальными браузерами?
Правильно! В обоих случаях нужно делать транспиляцию зависимостей.
Транспилируем вручную
Разберем на примере конфигурации webpack в паре с babel-loader. Типичный пример выглядит примерно так:
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }]
]
}
}
}
]
}
В документации и примерах использования Babel и babel-loader рекомендуют исключать node_modules
из файлов для транспиляции (exclude: /node_modules/
), чтобы сборка выполнялась быстрее. Удалив эту строчку, мы включим транспиляцию зависимостей, но пожертвуем скоростью сборки. Есть компромиссный вариант: если мы знаем, какие именно зависимости нужно транспилировать, то можем указать только их:
exclude: _ => /node_modules/.test(_) && !/node_modules\/(nanostores|p-limit)/.test(_)
Или можем выбрать только файлы с определённым расширением:
exclude: _ => /node_modules/.test(_) && !/(\.babel\.js|\.mjs|\.es)$/.test(_)
Как будет различаться размер бандла и время сборки с разными настройками? Рассмотрим на примере бандла с тремя очень разными зависимостями:
- p-limit (использует самые последние фишки JavaScript, включая приватные поля класса, которые поддерживаются не везде)
- axios (ES5-код)
- и svelte (использует актуальные фишки JavaScript, такие как стрелочные функции)
Конфигурация | Транспиляция | Совместимость | Размер бандла | Время сборки |
---|---|---|---|---|
Базовая | Не выполняется | Невозможно предсказать, с какими браузерами совместим бандл | 21 КБ | 1,8 с |
target: defaults and supports es6-module | До ES6. Приватные поля классов будут даунгрейднуты, стрелочные функции и классы останутся | Новые браузеры | 22 КБ | 2,6 с |
target: defaults с полифилами | До ES5 | Все браузеры | 123 КБ | 6,1 с |
Общее время сборки двух бандлов с помощью babel-loader составило 8,7 с. (Надеюсь, понятно, что в нашем примере, без транспиляции, получившийся бандл не будет совместим со старыми браузерами из-за p-limit.)
(Кстати, про сборку нескольких бандлов под разные браузеры подробно написано в другой моей статье.)
Но что делать, если вам не хочется вручную указывать нужные файлы и пакеты в конфигурации? Есть готовый и очень удобный инструмент!
Транспилируем с помощью optimize-plugin
optimize-plugin для webpack от Джейсона Миллера из Google (@_developit) сделает за вас всё, что нужно, и даже чуть больше:
- оптимизирует ваш код и код всех зависимостей
- если нужно, сгенерирует два бандла (для новых и старых браузеров), используя подход module/nomodule
- а ещё может сделать апгрейд ES5-кода до ES6, используя babel-preset-modernize!
Вот какие результаты будут у optimize-plugin для нашего бандла с тремя зависимостям:
Конфигурация | Транспиляция | Совместимость | Размер бандла | Время сборки |
---|---|---|---|---|
Базовая | Одновременно до ES6 и до ES5 с полифилами | Все браузеры | 20 КБ для новых браузеров 92 КБ для старых браузеров (из них 67 КБ — полифилы) |
7,6 с |
Общее время сборки двух бандлов с помощью optimize-plugin составило 7,6 с. Как видно, optimize-plugin не только быстрее babel-loader, но и создаёт бандл меньшего размера. Можете проверить сами.
Почему optimize-plugin выигрывает
Выигрыш в скорости получается за счёт того, что код анализируется и собирается не два раза, а один, после чего optimize-plugin транспилирует получившийся бандл под новые и старые браузеры.
Получить выигрыш в размере позволяет babel-preset-modernize. Если в своём коде вы, скорее всего, используете все фишки ES6+, то в зависимостях может быть всё что угодно. Поскольку optimize-plugin работает с уже собранным бандлом, который содержит код всех зависимостей, их код тоже будет транспилирован.
Вот пример работы babel-preset-modernize. Предположим, мы написали такой код:
const items = [{
id: 0,
price: 400
}, {
id: 1,
price: 300
}, {
id: 2,
price: 550
}];
const sum = items.reduce(function (sum, item) {
const price = item.price;
return sum + price;
}, 0);
console.log(sum);
После транспиляции получим:
const items = [{
id: 0,
price: 400
}, {
id: 1,
price: 300
}, {
id: 2,
price: 550
}];
const sum = items.reduce((sum, {
price
}) => sum + price, 0);
console.log(sum);
Что поменялось:
- обычная анонимная функция заменена на стрелочную функцию
- доступ к свойству
item.price
заменён деструктуризацией аргумента функции
Размер кода уменьшился с 221 байта до 180 байт. При этом здесь выполняется всего два типа трансформаций, но babel-preset-modernize умеет делать больше.
Что дальше?
Плагин демонстрирует очень крутые результаты, но ему ещё есть, куда расти. Недавно я сделал несколько улучшений, в том числе добавил поддержку webpack 5.
Если вас заинтересовал optimize-plugin, то призываю попробовать его для сборки своих приложений, а также внести свой вклад в его развитие.
В любом случае, транспилируйте зависимости своего кода, чтобы контролировать его совместимость со старыми и новыми браузерами.
Кроме того, если вы создаете приложение для работы с данными, обратите внимание на Cube. Он поможет вам создать API для метрик, которые вы сможете использовать в своем приложении за считанные минуты.
Top comments (0)