Публикация, доставка и установка современного JavaScript для ускорения работы приложений
Повысьте производительность, включив зависимости и выходные данные для современного JavaScript.
Более 90% браузеров способны выполнять современный JavaScript, но на сегодняшний день одна из основных причин проблем с производительностью в Интернете— преобладание старого JavaScript. С помощью несложного средства EStimator.dev можно вычислить, насколько уменьшится размер и повысится производительность сайта при использовании синтаксиса современного JavaScript.

На сегодняшний день возможности Интернета ограничены старым JavaScript. Никакие методы оптимизации не повысят производительность так, как это можно сделать, создавая, публикуя и доставляя веб-страницы или пакеты с использованием синтаксиса ES2017.
Современный JavaScript #
Современный JavaScript — это не просто код, написанный в соответствии с определенной версией спецификации ECMAScript; это код, синтаксис которого поддерживается во всех современных браузерах. Современные веб-браузеры, например Chrome, Edge, Firefox и Safari, занимают более 90% рынка, а доля других браузеров с такими же базовыми модулями рендеринга составляет еще 5%. Это означает, что 95% веб-трафика в мире поступает из браузеров, поддерживающих самые широко используемые возможности языка JavaScript за последние 10 лет (в том числе указанные ниже).
- Классы (ES2015)
- Стрелочные функции (ES2015)
- Генераторы (ES2015)
- Области действия блоков (ES2015)
- Деструктуризация (ES2015)
- Параметры операторов rest и spread (ES2015)
- Сокращенная запись объектов (ES2015)
- Ключевые слова async и await (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, не нарушая работу приложения в старых браузерах, но общая идея заключается в том, чтобы система сборки транспилировала зависимости к тому же синтаксису, который используется для исходного кода.
webpack #
В webpack 5 появилась возможность указывать, какой синтаксис необходимо использовать при генерации кода для пакетов и модулей. Это не приводит к транспилированию кода или зависимостей и влияет только на связующий код, генерируемый webpack. Чтобы указать цель поддержки для браузеров, добавьте в проект конфигурацию browserslist или сделайте это непосредственно в конфигурации webpack:
module.exports = {
target: ['web', 'es2017'],
};
Кроме того, можно настроить средство webpack для создания оптимизированных пакетов, предназначенных для современной среды ES Modules, без ненужных функций-оболочек. В результате webpack будет загружать пакеты с разделенным кодом, используя поле <script type="module">
.
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
Некоторые плагины webpack, например Optimize Plugin и BabelEsmPlugin, позволяют компилировать и доставлять современный код JavaScript, поддерживая при этом старые браузеры.
Плагин Optimize Plugin #
Optimize Plugin — это плагин для webpack, который преобразует окончательный собранный в пакеты код из современного JavaScript в старый (а не создает отдельные файлы с исходным кодом). Это автономная настройка, которая указывает средству webpack, что код написан на современном JavaScript, и не нужно разделять его на несколько выходных файлов или синтаксисов.
Так как плагин Optimize Plugin работает с пакетами, а не с отдельными модулями, он одинаково обрабатывает и код приложения, и зависимости. Это позволяет безопасно использовать зависимости современного JavaScript из npm, так как их код будет помещен в пакет и транспилирован в правильный синтаксис. Кроме того, этот метод может работать быстрее, чем традиционные решения, включающие два этапа компиляции, позволяя при этом создавать отдельные пакеты для современных и устаревших браузеров. Эти два набора пакетов предназначены для загрузки с использованием шаблона module/nomodule.
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Плагин Optimize Plugin
может работать быстрее и эффективнее, чем пользовательские конфигурации webpack, в которых современный и старый код обычно помещают в отдельные пакеты. Кроме того, он управляет работой компилятора Babel и уменьшает размер пакетов с помощью средства Terser, используя отдельные оптимальные параметры для современного и старого выходного кода. И, наконец, полифиллы, необходимые для сгенерированных старых пакетов, извлекаются в отдельный скрипт и поэтому они никогда не дублируются, а новые браузеры не загружают их, когда это не нужно.
Плагин BabelEsmPlugin #
BabelEsmPlugin — это плагин для webpack, который работает с настройкой @babel/preset-env и дает возможность создавать современные версии существующих пакетов. Это позволяет доставлять код с уменьшенной степенью транспиляции в современные браузеры. Это самое популярное готовое решение для атрибутов module/nomodule, используемое в Next.js и Preact CLI.
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// Существующая конфигурация babel-loader:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
Плагин BabelEsmPlugin
поддерживает широкий спектр конфигураций webpack, так как он создает две в значительной степени разные сборки приложения. На двойную компиляцию больших приложений может потребоваться немного дополнительного времени, но благодаря этому методу подключаемый модуль BabelEsmPlugin
можно легко интегрировать в существующие конфигурации webpack, и он представляет собой один из самых удобных из доступных вариантов.
Настройка babel-loader для транспилирования node_modules #
Если вы используете babel-loader
без одного из двух указанных выше подключаемых модулей, то чтобы можно было использовать современные модули npm JavaScript, необходимо выполнить важное действие. Настроив две отдельные конфигурации babel-loader
, вы сможете автоматически компилировать функции современного языка, имеющиеся в node_modules
, в ES2017, при этом транспилириуя ваш собственный код с помощью подключаемых модулей Babel и предварительных настроек, заданных в конфигурации проекта. При этом не будут сгенерированы современные и старые пакеты для настройки module/nomodule, но можно будет устанавливать и использовать пакеты npm, содержащие современный JavaScript, не нарушая работу старых браузеров.
В webpack-plugin-modern-npm этот метод используется для компиляции зависимостей npm, у которых есть поле "exports"
в соответствующих файлах package.json
, так как в них может содержаться современный синтаксис:
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// Автоматическое транспилирование современного кода, содержащегося в node_modules
new ModernNpmPlugin(),
],
};
Кроме того, можно реализовать этот метод вручную в конфигурации webpack, проверяя поле "exports"
в файлах package.json
модулей по мере их разрешения. Если для краткости исключить кэширование, пользовательская реализация может выглядеть следующим образом:
// webpack.config.js
module.exports = {
module: {
rules: [
// Транспилирование собственного кода:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Транспилирование современных зависимостей:
{
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 генерирует современный код. В результате это средство можно настроить для создания современных и старых пакетов с помощью официальных плагинов, которые вы, вероятно, уже используете.
@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: [
// Современные пакеты:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// Старые пакеты (ES5):
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
Дополнительные средства сборки #
В Rollup и webpack можно настраивать много параметров. Поэтому чтобы использовать современный синтаксис JavaScript в зависимостях, как правило, необходимо обновлять конфигурацию каждого проекта. Кроме того, существуют средства сборки более высокого уровня, например Parcel, Snowpack, Vite и WMR, в которых предпочтение отдается соглашениям и настройкам, используемым по умолчанию, а не конфигурации. В большинстве таких средств предполагается, что зависимости npm могут содержать современный синтаксис. Соответственно, при сборке кода для рабочей среды эти средства транспилируют зависимости в соответствующие уровни синтаксиса.
Помимо специализированных плагинов для webpack и Rollup для добавления современных пакетов JavaScript с резервным устаревшим кодом в любые проекты можно использовать devolution. Devolution — это автономное средство, которое преобразует выходные данные системы сборки и создает варианты кода на старом JavaScript. Это позволяет выполнять преобразования и объединение в пакеты, чтобы получать современные выходные данные.
Вывод #
С помощью средства EStimator.dev можно легко оценить эффект от перехода на современный код JavaScript для большинства ваших пользователей. На сегодняшний день спецификация ES2017 ближе всего к современному синтаксису, и некоторые средства, например npm, Babel, webpack и Rollup, позволяют настраивать систему сборки и писать пакеты с использованием этого синтаксиса. В данной публикации рассказывается о нескольких подходах, и вам следует выбрать самый простой вариант, подходящий для ваших целей.