В этой лабораторной работе повысьте производительность простого приложения, позволяющего пользователям оценивать случайных кошек. Узнайте, как оптимизировать JavaScript-архив, минимизировав объём транспилируемого кода.
В этом примере приложения вы можете выбрать слово или эмодзи, чтобы выразить свою любовь к каждому коту. При нажатии кнопки приложение отображает её значение под текущим изображением кота.
Мера
Всегда полезно начать с проверки веб-сайта, прежде чем добавлять какие-либо оптимизации:
- Для предварительного просмотра сайта нажмите «Просмотреть приложение» . Затем нажмите «Полный экран».
.
- Нажмите `Control+Shift+J` (или `Command+Option+J` на Mac), чтобы открыть DevTools.
- Откройте вкладку Сеть .
- Установите флажок Отключить кэш .
- Перезагрузите приложение.
Это приложение занимает более 80 КБ! Время проверить, не используются ли какие-либо части пакета:
Нажмите
Control+Shift+P
(илиCommand+Shift+P
на Mac), чтобы открыть меню команд .Введите
Show Coverage
и нажмитеEnter
, чтобы отобразить вкладку «Покрытие» .На вкладке «Покрытие» нажмите кнопку «Перезагрузить» , чтобы перезагрузить приложение и одновременно фиксировать покрытие.
Посмотрите, сколько кода было использовано по сравнению с объемом загрузки для основного пакета:
Более половины пакета (44 КБ) даже не используется. Это связано с тем, что большая часть кода внутри состоит из полифиллов, обеспечивающих работу приложения в старых браузерах.
Используйте @babel/preset-env
Синтаксис языка JavaScript соответствует стандарту ECMAScript, или ECMA-262 . Новые версии спецификации выпускаются каждый год и включают новые функции, прошедшие процедуру утверждения. Каждый основной браузер находится на разном этапе поддержки этих функций.
В приложении используются следующие функции ES2015:
Также используется следующая функция ES2017:
Не стесняйтесь заглянуть в исходный код в src/index.js
чтобы увидеть, как все это используется.
Все эти функции поддерживаются в последней версии Chrome, но как быть с другими браузерами, которые их не поддерживают? Babel , входящий в состав приложения, — самая популярная библиотека для компиляции кода с новым синтаксисом в код, понятный старым браузерам и средам. Это достигается двумя способами:
- Полифиллы включены для эмуляции новых функций ES2015+, чтобы их API можно было использовать, даже если они не поддерживаются браузером. Вот пример полифилла для метода
Array.includes
. - Плагины используются для преобразования кода ES2015 (или более поздней версии) в более старый синтаксис ES5. Поскольку эти изменения связаны с синтаксисом (например, стрелочные функции), их нельзя эмулировать с помощью полифиллов.
Посмотрите на package.json
, чтобы узнать, какие библиотеки Babel включены:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
-
@babel/core
— это основной компилятор Babel. Благодаря ему все конфигурации Babel определяются в файле.babelrc
в корне проекта. -
babel-loader
включает Babel в процесс сборки webpack.
Теперь посмотрим на webpack.config.js
, чтобы увидеть, как правило, включается babel-loader
:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
-
@babel/polyfill
предоставляет все необходимые полифиллы для любых новых функций ECMAScript, чтобы они могли работать в средах, где они не поддерживаются. Он уже импортирован в самом верху файлаsrc/index.js.
import "./style.css";
import "@babel/polyfill";
-
@babel/preset-env
определяет, какие преобразования и полифилы необходимы для любых браузеров или сред, выбранных в качестве целевых.
Взгляните на файл конфигурации Babel, .babelrc
, чтобы увидеть, как он включен:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
Это конфигурация Babel и Webpack. Узнайте, как включить Babel в своё приложение, если вы используете другой сборщик модулей, а не Webpack.
Атрибут targets
в .babelrc
определяет, какие браузеры являются целевыми. @babel/preset-env
интегрируется с browserslist, что означает, что вы можете найти полный список совместимых запросов, которые можно использовать в этом поле, в документации по browserlist .
Значение "last 2 versions"
транспилирует код в приложение для последних двух версий каждого браузера.
Отладка
Чтобы получить полное представление обо всех целях Babel браузера, а также обо всех включенных преобразованиях и полифиллах, добавьте поле debug
в .babelrc:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- Нажмите «Инструменты» .
- Нажмите Журналы .
Перезагрузите приложение и просмотрите журналы состояния сбоев в нижней части редактора.
Целевые браузеры
Babel выводит на консоль ряд сведений о процессе компиляции, включая все целевые среды, для которых был скомпилирован код.
Обратите внимание, что в этот список включены браузеры, поддержка которых прекращена, такие как Internet Explorer. Это проблема, поскольку в неподдерживаемые браузеры новые функции не добавляются, и Babel продолжает транспилировать для них специфический синтаксис. Это неоправданно увеличивает размер вашего пакета, если пользователи не используют этот браузер для доступа к вашему сайту.
Babel также регистрирует список использованных плагинов преобразования:
Довольно длинный список! Это все плагины, которые Babel должен использовать для преобразования любого синтаксиса ES2015+ в более старый синтаксис для всех целевых браузеров.
Однако Babel не показывает какие-либо конкретные используемые полифиллы:
Это связано с тем, что весь @babel/polyfill
импортируется напрямую.
Загружайте полифиллы по отдельности
По умолчанию Babel включает все полифиллы, необходимые для полноценной среды ES2015+, при импорте @babel/polyfill
в файл. Чтобы импортировать конкретные полифиллы, необходимые для целевых браузеров, добавьте в конфигурацию запись useBuiltIns: 'entry'
.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
Перезагрузите приложение. Теперь вы можете увидеть все включённые полифиллы:
Хотя теперь включены только необходимые полифиллы для "last 2 versions"
, список всё ещё очень длинный! Это связано с тем, что полифиллы, необходимые для целевых браузеров для каждой новой функции, по-прежнему включены. Измените значение атрибута на usage
, чтобы включить только те, которые необходимы для функций, используемых в коде.
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
Благодаря этому полифиллы будут автоматически подключаться при необходимости. Это означает, что вы можете удалить импорт @babel/polyfill
в src/index.js.
import "./style.css";
import "@babel/polyfill";
Теперь включены только необходимые для приложения полифилы.
Размер пакета приложения существенно уменьшен.
Сужение списка поддерживаемых браузеров
Количество целевых браузеров всё ещё довольно велико, и лишь немногие пользователи используют устаревшие браузеры, такие как Internet Explorer. Обновите конфигурации следующим образом:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
Ознакомьтесь с подробностями полученного набора.
Поскольку приложение небольшое, эти изменения не сильно влияют на результат. Однако рекомендуется использовать процентную долю рынка браузеров (например, ">0.25%"
) и исключить определённые браузеры, которые, как вы уверены, ваши пользователи не используют. Подробнее об этом можно узнать в статье Джеймса Кайла «Последние 2 версии считаются вредоносными» .
Используйте <script type="module">
Есть ещё много возможностей для улучшения. Хотя ряд неиспользуемых полифиллов был удалён, многие из них поставляются, но не нужны некоторым браузерам. Благодаря модулям новый синтаксис можно писать и напрямую добавлять в браузеры, без использования ненужных полифиллов.
Модули JavaScript — относительно новая функция, поддерживаемая всеми основными браузерами . Модули можно создавать с помощью атрибута type="module"
для определения скриптов, импортирующих и экспортирующих данные из других модулей. Например:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
Многие новые функции ECMAScript уже поддерживаются в средах, поддерживающих модули JavaScript (вместо того, чтобы требовать Babel). Это означает, что конфигурацию Babel можно изменить для отправки двух разных версий вашего приложения в браузер:
- Версия, которая будет работать в новых браузерах, поддерживающих модули, и которая включает в себя модуль, который в значительной степени не транспилируется, но имеет меньший размер файла.
- Версия, включающая в себя более крупный транспилированный скрипт, который будет работать в любом устаревшем браузере.
Использование модулей ES с Babel
Чтобы использовать отдельные настройки @babel/preset-env
для двух версий приложения, удалите файл .babelrc
. Настройки Babel можно добавить в конфигурацию Webpack, указав два разных формата компиляции для каждой версии приложения.
Начните с добавления конфигурации для устаревшего скрипта в webpack.config.js
:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
Обратите внимание, что вместо значения targets
для "@babel/preset-env"
используется esmodules
со значением false
. Это означает, что Babel включает все необходимые преобразования и полифиллы для работы с любым браузером, который ещё не поддерживает ES-модули.
Добавьте объекты entry
, cssRule
и corePlugins
в начало файла webpack.config.js
. Все они являются общими как для модуля, так и для устаревших скриптов, обслуживаемых браузером.
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
Теперь аналогичным образом создайте объект конфигурации для скрипта модуля ниже, где определен legacyConfig
:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
Главное отличие здесь заключается в том, что для имени выходного файла используется расширение .mjs
. Значение esmodules
здесь установлено в true, что означает, что код, выводимый в этот модуль, представляет собой более компактный и менее скомпилированный скрипт, который не подвергается каким-либо преобразованиям в данном примере, поскольку все используемые функции уже поддерживаются браузерами, поддерживающими модули.
В самом конце файла экспортируйте обе конфигурации в один массив.
module.exports = [
legacyConfig, moduleConfig
];
Теперь он создает как меньший модуль для браузеров, которые его поддерживают, так и больший транспилированный скрипт для старых браузеров.
Браузеры, поддерживающие модули, игнорируют скрипты с атрибутом nomodule
. Браузеры, не поддерживающие модули, наоборот, игнорируют элементы скрипта с type="module"
. Это означает, что вы можете включить как модуль, так и скомпилированный резервный вариант. В идеале обе версии приложения должны быть представлены в index.html
следующим образом:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
Браузеры, поддерживающие модули, загружают и выполняют main.mjs
и игнорируют main.bundle.js.
Браузеры, не поддерживающие модули, делают наоборот.
Важно отметить, что, в отличие от обычных скриптов, модульные скрипты всегда откладываются по умолчанию. Если вы хотите, чтобы эквивалентный скрипт nomodule
также откладывался и выполнялся только после парсинга, необходимо добавить атрибут defer
:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
Последнее, что нужно здесь сделать, — это добавить атрибуты module
и nomodule
к модулю и устаревшему скрипту соответственно, импортируйте ScriptExtHtmlWebpackPlugin в самый верх webpack.config.js
:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
Теперь обновите массив plugins
в конфигурациях, чтобы включить этот плагин:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
Эти настройки плагина добавляют атрибут type="module"
для всех элементов скрипта .mjs
, а также атрибут nomodule
для всех модулей скрипта .js
.
Обслуживание модулей в HTML-документе
Последнее, что нужно сделать, — это вывести элементы скрипта как старого, так и современного формата в HTML-файл. К сожалению, плагин HTMLWebpackPlugin
, создающий итоговый HTML-файл, в настоящее время не поддерживает вывод скриптов как module, так и nomodule. Хотя существуют обходные пути и отдельные плагины, созданные для решения этой проблемы, такие как BabelMultiTargetPlugin и HTMLWebpackMultiBuildPlugin , в данном руководстве используется более простой подход — добавление элемента скрипта module вручную.
Добавьте следующее в src/index.js
в конце файла:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
Теперь загрузите приложение в браузере, который поддерживает модули, например, в последней версии Chrome.
Извлекается только модуль, причём размер пакета гораздо меньше, поскольку он практически не транспилируется! Другой элемент скрипта полностью игнорируется браузером.
При загрузке приложения в старом браузере будет загружен только более крупный транспилированный скрипт со всеми необходимыми полифилами и преобразованиями. Вот скриншот всех запросов, выполненных в старой версии Chrome (версия 38).
Заключение
Теперь вы понимаете, как использовать @babel/preset-env
для предоставления только необходимых полифиллов для целевых браузеров. Вы также знаете, как модули JavaScript могут дополнительно повысить производительность, предоставляя две разные транспилированные версии приложения. Разобравшись, как оба этих метода могут значительно сократить размер вашего пакета, приступайте к оптимизации!