Публикуйте, отправляйте и устанавливайте современный JavaScript для ускорения работы приложений.

Повысьте производительность, включив современные зависимости и вывод JavaScript.

Более 90% браузеров способны использовать современный JavaScript, но преобладание устаревшего JavaScript остается сегодня серьезным источником проблем с производительностью в Интернете.

Современный JavaScript

Современный JavaScript характеризуется не как код, написанный в определенной версии спецификации ECMAScript, а скорее как код, написанный с синтаксисом, который поддерживается всеми современными браузерами. Современные веб-браузеры, такие как Chrome, Edge, Firefox и Safari, составляют более 90% рынка браузеров , а различные браузеры, использующие одни и те же базовые механизмы рендеринга, составляют еще 5%. Это означает, что 95% глобального веб-трафика поступает из браузеров, которые поддерживают наиболее широко используемые функции языка JavaScript за последние 10 лет, в том числе:

  • Классы (ES2015)
  • Стрелочные функции (ES2015)
  • Генераторы (ES2015)
  • Область видимости блока (ES2015)
  • Деструктуризация (ES2015)
  • Параметры покоя и распространения (ES2015)
  • Сокращение объекта (ES2015)
  • Асинхронность/ожидание (ES2017)

Функции новых версий спецификации языка обычно менее последовательно поддерживаются в современных браузерах. Например, многие функции ES2020 и ES2021 поддерживаются только в 70% рынка браузеров — это все еще большинство браузеров, но недостаточно, чтобы можно было напрямую полагаться на эти функции. Это означает, что, хотя «современный» JavaScript является движущейся целью, ES2017 имеет самый широкий диапазон совместимости с браузерами , в то же время включая большинство часто используемых современных синтаксических функций . Другими словами, ES2017 на сегодняшний день наиболее близок к современному синтаксису .

Устаревший JavaScript

Устаревший JavaScript — это код, который специально избегает использования всех вышеперечисленных функций языка. Большинство разработчиков пишут свой исходный код, используя современный синтаксис, но компилируют все с использованием устаревшего синтаксиса для увеличения поддержки браузеров. Компиляция с устаревшим синтаксисом действительно увеличивает поддержку браузеров, однако эффект часто меньше, чем мы думаем. Во многих случаях поддержка увеличивается примерно с 95% до 98%, но требует значительных затрат:

  • Устаревший JavaScript обычно примерно на 20% больше и медленнее, чем эквивалентный современный код. Недостатки инструментов и неправильная конфигурация часто еще больше увеличивают этот разрыв.

  • Установленные библиотеки составляют до 90% типичного производственного кода JavaScript. Библиотечный код требует еще больших накладных расходов на устаревший JavaScript из-за дублирования полифилов и хелперов, которого можно было бы избежать, публикуя современный код.

Современный JavaScript на npm

Недавно Node.js стандартизировал поле "exports" для определения точек входа для пакета :

{
  "exports": "./index.js"
}

Модули, на которые указывает поле "exports" подразумевают версию Node не ниже 12.8, которая поддерживает ES2019. Это означает, что любой модуль, на который ссылается поле "exports" может быть написан на современном JavaScript . Потребители пакетов должны предполагать, что модули с полем "exports" содержат современный код, и при необходимости транспилировать.

Только современный

Если вы хотите опубликовать пакет с современным кодом и предоставить потребителю возможность его транспилирования, когда он будет использовать его в качестве зависимости, используйте только поле "exports" .

{
  "name": "foo",
  "exports": "./modern.js"
}

Современный вариант с устаревшим резервным вариантом

Используйте поле "exports" вместе с полем "main" , чтобы опубликовать свой пакет с использованием современного кода, а также включите резервную версию ES5 + CommonJS для устаревших браузеров.

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

Современный вариант с устаревшим резервным вариантом и оптимизацией пакета ESM.

Помимо определения резервной точки входа CommonJS, поле "module" можно использовать для указания на аналогичный устаревший резервный пакет, но использующий синтаксис модуля JavaScript ( import и export ).

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Многие упаковщики, такие как Webpack и Rollup, используют это поле, чтобы воспользоваться преимуществами функций модуля и включить встряхивание дерева . Это по-прежнему устаревший пакет, который не содержит никакого современного кода, кроме синтаксиса import / export , поэтому используйте этот подход для поставки современного кода с устаревшим резервным вариантом, который все еще оптимизирован для объединения.

Современный JavaScript в приложениях

Сторонние зависимости составляют подавляющее большинство типичного производственного кода JavaScript в веб-приложениях. Хотя зависимости npm исторически публиковались как устаревший синтаксис ES5, это больше не является безопасным предположением и рискует привести к тому, что обновления зависимостей нарушат поддержку браузера в вашем приложении.

Поскольку все больше пакетов npm переходят на современный JavaScript, важно убедиться, что инструменты сборки настроены для их обработки. Вполне вероятно, что некоторые из пакетов npm, от которых вы зависите, уже используют современные языковые функции. Существует ряд вариантов использования современного кода из npm без нарушения работы вашего приложения в старых браузерах, но общая идея состоит в том, чтобы система сборки переносила зависимости в тот же целевой синтаксис, что и ваш исходный код.

веб-пакет

Начиная с веб-пакета 5, теперь можно настроить, какой синтаксис будет использовать веб-пакет при создании кода для пакетов и модулей. Это не переносит ваш код или зависимости, а влияет только на «связывающий» код, созданный веб-пакетом. Чтобы указать цель поддержки браузера, добавьте конфигурацию списка браузеров в свой проект или сделайте это непосредственно в конфигурации вашего веб-пакета:

module.exports = {
  target: ['web', 'es2017'],
};

Также можно настроить веб-пакет для создания оптимизированных пакетов, в которых отсутствуют ненужные функции-оболочки при ориентации на современную среду ES-модулей. Это также настраивает веб-пакет для загрузки пакетов с разделением кода с помощью <script type="module"> .

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

Доступен ряд плагинов веб-пакетов, которые позволяют компилировать и поставлять современный JavaScript, сохраняя при этом поддержку устаревших браузеров, например Optimize Plugin и BabelEsmPlugin.

Плагин оптимизации

Optimize Plugin — это плагин веб-пакета, который преобразует окончательный связанный код из современного JavaScript в устаревший вместо каждого отдельного исходного файла. Это автономная установка, которая позволяет конфигурации вашего веб-пакета предполагать, что все представляет собой современный JavaScript без специального ветвления для нескольких выходных данных или синтаксисов.

Поскольку плагин Optimize работает с пакетами, а не с отдельными модулями, он одинаково обрабатывает код вашего приложения и ваши зависимости. Это делает безопасным использование современных зависимостей JavaScript от npm, поскольку их код будет упакован и транспилирован в правильный синтаксис. Это также может быть быстрее, чем традиционные решения, включающие два этапа компиляции, но при этом генерирующие отдельные пакеты для современных и устаревших браузеров. Два набора пакетов предназначены для загрузки с использованием шаблона модуль/номодуль .

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin может быть быстрее и эффективнее, чем пользовательские конфигурации веб-пакетов, которые обычно объединяют современный и устаревший код отдельно. Он также управляет запуском Babel и минимизирует пакеты с помощью Terser с отдельными оптимальными настройками для современных и устаревших результатов. Наконец, полифилы, необходимые для созданных устаревших пакетов, извлекаются в специальный скрипт, поэтому они никогда не дублируются и не загружаются без необходимости в новых браузерах.

Сравнение: двойная транспиляция исходных модулей и транспиляция сгенерированных пакетов.

BabelEsmПлагин

BabelEsmPlugin — это плагин веб-пакета, который работает вместе с @babel/preset-env для создания современных версий существующих пакетов и доставки меньшего количества транспилируемого кода в современные браузеры. Это самое популярное готовое решение для модуля/номодуля, используемое Next.js и Preact CLI .

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin поддерживает широкий спектр конфигураций веб-пакетов, поскольку он запускает две практически отдельные сборки вашего приложения. Двойная компиляция может занять немного больше времени для больших приложений, однако этот метод позволяет BabelEsmPlugin легко интегрироваться в существующие конфигурации веб-пакетов и делает его одним из наиболее удобных доступных вариантов.

Настройте Babel-Loader для передачи node_modules

Если вы используете babel-loader без одного из двух предыдущих плагинов, необходимо сделать важный шаг для использования современных модулей JavaScript npm. Определение двух отдельных конфигураций babel-loader позволяет автоматически компилировать современные языковые функции, найденные в node_modules в ES2017, при этом транслируя собственный собственный код с помощью плагинов Babel и предустановок, определенных в конфигурации вашего проекта. Это не создает современные и устаревшие пакеты для установки модуля/номодуля, но позволяет устанавливать и использовать пакеты npm, содержащие современный JavaScript, не нарушая работу старых браузеров.

webpack-plugin-modern-npm использует этот метод для компиляции зависимостей npm, у которых есть поле "exports" в их package.json , поскольку они могут содержать современный синтаксис:

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

Альтернативно, вы можете реализовать этот метод вручную в конфигурации вашего веб-пакета, проверяя наличие поля "exports" в package.json модулей по мере их разрешения. Если для краткости опустить кэширование, пользовательская реализация может выглядеть так:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

При использовании этого подхода вам необходимо убедиться, что ваш минификатор поддерживает современный синтаксис. И Terser , и uglify-es имеют возможность указать {ecma: 2017} , чтобы сохранить, а в некоторых случаях генерировать синтаксис ES2017 во время сжатия и форматирования.

Свернуть

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

@rollup/plugin-babel

Если вы используете Rollup, метод getBabelOutputPlugin() (предоставляемый официальным плагином Babel Rollup) преобразует код в сгенерированные пакеты, а не в отдельные исходные модули. Rollup имеет встроенную поддержку создания нескольких наборов пакетов в рамках одной сборки, каждый из которых имеет свои собственные плагины. Вы можете использовать это для создания различных пакетов для современных и устаревших версий, пропуская каждый из них через другую конфигурацию выходного плагина Babel:

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

Дополнительные инструменты сборки

Накопительный пакет и веб-пакет обладают широкими возможностями настройки, что обычно означает, что каждый проект должен обновить свою конфигурацию, включить современный синтаксис JavaScript в зависимостях. Существуют также инструменты сборки более высокого уровня, которые отдают предпочтение соглашениям и настройкам по умолчанию, например Parcel , Snowpack , Vite и WMR . Большинство этих инструментов предполагают, что зависимости npm могут содержать современный синтаксис, и переносят их на соответствующие уровни синтаксиса при сборке для производства.

В дополнение к выделенным плагинам для Webpack и Rollup, в любой проект с помощью передачи полномочий можно добавить современные пакеты JavaScript с устаревшими резервными версиями. Деволюция — это автономный инструмент, который преобразует выходные данные системы сборки для создания устаревших вариантов JavaScript, позволяя объединять и преобразовывать их в современную цель вывода.