DEV Community

Cover image for Почему и как нужно транспилировать зависимости
Dan Onoshko
Dan Onoshko

Posted on • Edited on • Originally published at github.com

Почему и как нужно транспилировать зависимости

Если вы разработчик сайта, то наверняка пользуетесь каким-либо сборщиком (например, 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' }]
          ]
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

В документации и примерах использования Babel и babel-loader рекомендуют исключать node_modules из файлов для транспиляции (exclude: /node_modules/), чтобы сборка выполнялась быстрее. Удалив эту строчку, мы включим транспиляцию зависимостей, но пожертвуем скоростью сборки. Есть компромиссный вариант: если мы знаем, какие именно зависимости нужно транспилировать, то можем указать только их:

exclude: _ => /node_modules/.test(_) && !/node_modules\/(nanostores|p-limit)/.test(_)
Enter fullscreen mode Exit fullscreen mode

Или можем выбрать только файлы с определённым расширением:

exclude: _ => /node_modules/.test(_) && !/(\.babel\.js|\.mjs|\.es)$/.test(_)
Enter fullscreen mode Exit fullscreen mode

Как будет различаться размер бандла и время сборки с разными настройками? Рассмотрим на примере бандла с тремя очень разными зависимостями:

  • 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);
Enter fullscreen mode Exit fullscreen mode

После транспиляции получим:

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);
Enter fullscreen mode Exit fullscreen mode

Что поменялось:

  • обычная анонимная функция заменена на стрелочную функцию
  • доступ к свойству item.price заменён деструктуризацией аргумента функции

Размер кода уменьшился с 221 байта до 180 байт. При этом здесь выполняется всего два типа трансформаций, но babel-preset-modernize умеет делать больше.

Что дальше?

Плагин демонстрирует очень крутые результаты, но ему ещё есть, куда расти. Недавно я сделал несколько улучшений, в том числе добавил поддержку webpack 5.

Если вас заинтересовал optimize-plugin, то призываю попробовать его для сборки своих приложений, а также внести свой вклад в его развитие.

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

Кроме того, если вы создаете приложение для работы с данными, обратите внимание на Cube. Он поможет вам создать API для метрик, которые вы сможете использовать в своем приложении за считанные минуты.

Top comments (0)