Почему и как нужно транспилировать зависимости
Tags: javascript, webdev, perfomance
Published: 2022-02-22 08:05:27

---
title: Почему и как нужно транспилировать зависимости
published: true
description: Если вы разработчик сайта, то наверняка пользуетесь каким-либо сборщиком (например, Webpack, Rollup или Parcel), который к тому же транспилирует ваш JavaScript-код с помощью Babel. Ну и, конечно же, вы наверняка используете разнообразные зависимости, чтобы сократить время разработки. Обычно транспилировать код зависимостей не принято, ведь вроде как и без этого все прекрасно работает. Но времена меняются...
tags: javascript, webdev, perfomance
canonical_url: https://github.com/dangreen/dangreen/blob/main/posts/why-and-how-to-transpile-dependencies/ru.md
cover_image: https://res.cloudinary.com/practicaldev/image/fetch/s--QlKrfvzN--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3eg6a6gl5gxncnac99i.png
---

Если вы разработчик сайта, то наверняка пользуетесь каким-либо сборщиком (например, [Webpack](https://webpack.js.org), [Rollup](https://rollupjs.org/guide/en/) или [Parcel](https://parceljs.org)), который к тому же транспилирует ваш JavaScript-код с помощью [Babel](https://babeljs.io). Ну и, конечно же, вы наверняка используете разнообразные зависимости, чтобы сократить время разработки.

Обычно транспилировать код зависимостей не принято, ведь вроде как и без этого все прекрасно работает. Но времена меняются…

**(В этой статье я делюсь своим опытом работы над open source проектами. Это и мое хобби, и работа в [Cube](http://cube.dev/?utm_source=dev-to&utm_medium=post&utm_campaign=dependencies-transpilation), где мы создаем open source инструменты для создания приложений для работы с данными.)**

## Распространение ESM

До появления нативной поддержки ES-модулей в браузерах и в Node.js, npm-пакет мог содержать несколько вариантов кода:

-   [CommonJS](https://en.wikipedia.org/wiki/CommonJS)-вариант без использования [новых фишек](https://es6-features.org/#ExpressionBodies) JavaScript, таких как стрелочные функции. Такой код совместим с большинством версий Node.js и браузеров. Файл этого варианта указывают в поле `main` в `package.json`.
-   module-вариант, использующий ES6-импорты и экспорты, но так же без использования новых фишек. Модули позволяют делать [tree-shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking), то есть не включать в бандл неиспользуемый код. Файл этого варианта указывают в [поле `module`](https://stackoverflow.com/questions/42708484/what-is-the-module-package-json-field-for) в `package.json`.

Конечно, не все npm-пакеты сделаны по такому принципу — все зависит от разработчика. Однако браузерные и универсальные библиотеки часто распространяются именно так.

Сейчас ES-модули нативно поддерживаются [браузерами](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) уже больше трёх лет, а в [Node.js](https://nodejs.org/api/esm.html) их поддержка появилась в версии 12.20, выпущенной в ноябре 2020 года. Разработчики библиотек стали включать в npm-пакеты еще один вариант кода — для сред, нативно поддерживающих ES-модули, или вовсе перешли на поддержку только ES-модулей.

## Особенности ESM

Важно понимать, что нативные ES-модули — это не то же самое, что модули, использующие ES6-синтаксис импорта и экспорта:

-   То, как мы привыкли использовать `import` / `export`, не будет нигде работать нативно — такой код предназначен для дальнейшей обработки бандлером или транспайлером.

-   Нативные ESM не умеют резолвить расширения и index-файлы, поэтому их нужно явно указывать в пути:

    ```js
    // Неправильно:
    import _ from './utils'

    // Правильно:
    import _ from './utils/index.js'
    ```

-   Если пакет содержит ESM-код, то в `package.json` нужно явно указать тип пакета с помощью `"type": "module"`.

-   Для указания файла с ESM в `package.json` нужно использовать поле [`exports`](https://nodejs.org/api/packages.html#exports).

Более подробно про особенности ESM можно прочитать в [этой статье](https://2ality.com/2019/10/hybrid-npm-packages.html).

## Поддержка JavaScript

Еще одна особенность ESM это то, что мы точно знаем, с каких версий браузеры и Node.js поддерживают ES-модули. Соответственно, мы точно знаем, какие фишки JavaScript мы можем использовать в коде.

Например, все браузеры с нативной поддержкой ES-модулей имеют поддержку [стрелочных функций](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions), а значит нам больше не нужно избегать их использования или настраивать Babel для их транспиляции в обычные функции. Разработчики библиотек пользуются этой особенностью и используют в ESM-коде все новые фишки JavaScript.

## Транспиляция зависимостей

Но что делать, если ваш сайт должен работать в более старых браузерах? Или если какая-нибудь из зависимостей использует новые фишки JavaScript, не поддерживаемые актуальными браузерами?

Правильно! В обоих случаях нужно делать транспиляцию зависимостей.

### Транспилируем вручную

Разберем на примере конфигурации webpack в паре с [babel-loader](https://webpack.js.org/loaders/babel-loader/#usage). Типичный пример выглядит примерно так:

```js
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/`), чтобы сборка выполнялась быстрее. Удалив эту строчку, мы включим транспиляцию зависимостей, но пожертвуем скоростью сборки. Есть компромиссный вариант: если мы знаем, какие именно зависимости нужно транспилировать, то можем указать только их:

```js
exclude: _ => /node_modules/.test(_) && !/node_modules\/(nanostores|p-limit)/.test(_)
```

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

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

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

-   [p-limit](https://npmjs.com/package/p-limit) (использует самые последние фишки JavaScript, включая приватные поля класса, которые поддерживаются не везде)
-   [axios](https://npmjs.com/package/axios) (ES5-код)
-   и [svelte](https://www.npmjs.com/package/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.)

(Кстати, про сборку нескольких бандлов под разные браузеры подробно написано в [другой моей статье](https://web-standards.ru/articles/speed-up-with-browserslist/).)

Но что делать, если вам не хочется вручную указывать нужные файлы и пакеты в конфигурации? Есть готовый и очень удобный инструмент!

### Транспилируем с помощью optimize-plugin

[optimize-plugin](https://github.com/developit/optimize-plugin) для webpack от Джейсона Миллера из Google ([@_developit](https://twitter.com/_developit)) сделает за вас всё, что нужно, и даже чуть больше:

-   оптимизирует ваш код и код всех зависимостей
-   если нужно, сгенерирует два бандла (для новых и старых браузеров), используя [подход module/nomodule](https://web-standards.ru/articles/speed-up-with-browserslist/#section-7)
-   а ещё может сделать апгрейд ES5-кода до ES6, используя [babel-preset-modernize](https://github.com/developit/babel-preset-modernize)!

Вот какие результаты будут у optimize-plugin для нашего бандла с тремя зависимостям:

| Конфигурация | Транспиляция                              | Совместимость | Размер бандла                                                                     | Время сборки |
| ------------ | ----------------------------------------- | ------------- | --------------------------------------------------------------------------------- | ------------ |
| Базовая      | Одновременно до ES6 и до ES5 с полифилами | Все браузеры  | 20 КБ для новых браузеров<br>92 КБ для старых браузеров (из них 67 КБ — полифилы) | 7,6 с        |

Общее время сборки двух бандлов с помощью optimize-plugin составило 7,6 с. Как видно, optimize-plugin не только быстрее babel-loader, но и создаёт бандл меньшего размера. [Можете проверить сами](https://github.com/dangreen/optimize-plugin-demo).

### Почему optimize-plugin выигрывает

Выигрыш в скорости получается за счёт того, что код анализируется и собирается не два раза, а один, после чего optimize-plugin транспилирует получившийся бандл под новые и старые браузеры.

Получить выигрыш в размере позволяет [babel-preset-modernize](https://github.com/developit/babel-preset-modernize). Если в своём коде вы, скорее всего, используете все фишки ES6+, то в зависимостях может быть всё что угодно. Поскольку optimize-plugin работает с уже собранным бандлом, который содержит код всех зависимостей, их код тоже будет транспилирован.

Вот пример работы babel-preset-modernize. Предположим, мы написали такой код:

```js
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);
```

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

```js
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 умеет делать больше.

## Что дальше?

Плагин демонстрирует очень крутые результаты, но ему ещё есть, куда расти. Недавно я сделал [несколько улучшений](https://github.com/developit/optimize-plugin/releases/tag/1.1.0), в том числе добавил поддержку webpack 5.

Если вас заинтересовал optimize-plugin, то призываю [попробовать его](https://github.com/developit/optimize-plugin#install) для сборки своих приложений, а также внести свой вклад в его развитие.

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

**Кроме того, если вы создаете приложение для работы с данными, обратите внимание на [Cube](http://cube.dev/?utm_source=dev-to&utm_medium=post&utm_campaign=dependencies-transpilation). Он поможет вам создать API для метрик, которые вы сможете использовать в своем приложении за считанные минуты.**