Предоставляйте современный код современным браузерам для более быстрой загрузки страниц.

Хусейн Джирде
Хусейн Джирде

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

Скриншот приложения

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

Мера

Всегда полезно начать с проверки веб-сайта, прежде чем добавлять какие-либо оптимизации:

  1. Чтобы просмотреть сайт, нажмите «Просмотреть приложение» . Затем нажмите Полноэкранный режим полноэкранный .
  2. Нажмите «Control+Shift+J» (или «Command+Option+J» на Mac), чтобы открыть DevTools.
  3. Откройте вкладку «Сеть» .
  4. Установите флажок Отключить кеш .
  5. Перезагрузите приложение.

Запрос исходного размера пакета

Для этого приложения используется более 80 КБ! Пришло время узнать, не используются ли части пакета:

  1. Нажмите Control+Shift+P (или Command+Shift+P на Mac), чтобы открыть меню команд . Командное меню

  2. Введите Show Coverage и нажмите Enter , чтобы отобразить вкладку «Покрытие» .

  3. На вкладке «Покрытие» нажмите «Обновить» , чтобы перезагрузить приложение во время сбора данных о покрытии.

    Перезагрузите приложение с покрытием кода

  4. Посмотрите, сколько кода было использовано по сравнению с тем, сколько было загружено для основного пакета:

    Покрытие кода пакета

Более половины пакета (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.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 и веб-пакета. Узнайте, как включить Babel в свое приложение, если вы используете другой упаковщик модулей, чем веб-пакет.

Атрибут targets в .babelrc определяет, какие браузеры являются мишенью. @babel/preset-env интегрируется со списком браузеров, а это значит, что вы можете найти полный список совместимых запросов, которые можно использовать в этом поле, в документации по списку браузеров .

Значение "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";

Теперь включены только необходимые для приложения полифилы.

Список полифилов, включенных автоматически

Размер пакета приложений значительно уменьшен.

Размер пакета уменьшен до 30,1 КБ.

Сужение списка поддерживаемых браузеров

Число включенных целевых браузеров по-прежнему довольно велико, и немногие пользователи используют устаревшие браузеры, такие как Internet Explorer. Обновите конфигурации до следующего:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Взгляните на детали полученного комплекта.

Размер пакета 30,0 КБ.

Поскольку приложение настолько маленькое, эти изменения на самом деле не имеют большой разницы. Однако рекомендуется использовать процентную долю рынка браузеров (например, ">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.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-файл, в настоящее время не поддерживает вывод скриптов модуля и nomodule. Хотя для решения этой проблемы существуют обходные пути и отдельные плагины, такие как BabelMultiTargetPlugin и HTMLWebpackMultiBuildPlugin , для целей данного руководства используется более простой подход — добавление элемента сценария модуля вручную.

Добавьте следующее в src/index.js в конце файла:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Теперь загрузите приложение в браузере, поддерживающем модули, например в последней версии Chrome.

Модуль размером 5,2 КБ, полученный по сети для новых браузеров

Извлекается только модуль, но с гораздо меньшим размером пакета, поскольку он практически не транспилируется! Другой элемент сценария полностью игнорируется браузером.

Если вы загружаете приложение в более старом браузере, будет загружен только более крупный транспилированный скрипт со всеми необходимыми полифилами и преобразованиями. Вот скриншот всех запросов, сделанных в более старой версии Chrome (версия 38).

Скрипт размером 30 КБ получен для старых браузеров.

Заключение

Теперь вы понимаете, как использовать @babel/preset-env для предоставления только необходимых полифилов, необходимых для целевых браузеров. Вы также знаете, как модули JavaScript могут еще больше повысить производительность, предоставляя две разные транспилированные версии приложения. Имея хорошее представление о том, как оба эти метода могут значительно сократить размер вашего пакета, приступайте к оптимизации!