Используйте современный код в современных браузерах, чтобы ускорить загрузку страниц

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

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

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

Измерить

Прежде чем приступать к оптимизации, всегда полезно проверить сайт.

  1. Чтобы посмотреть сайт, нажмите View App (Посмотреть приложение), а затем – Fullscreen (Полноэкранный режим) полноэкранный режим.
  2. Нажмите Ctrl + Shift + J (или Cmd + Option + J на Mac), чтобы открыть Инструменты разработчика.
  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.

Теперь посмотрите на 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 в приложение.

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

Значение "last 2 versions" транспилирует код в приложении для двух последних версий каждого браузера.

Отладка

Чтобы получить полное представление обо всех целевых браузерах Babel, а также обо всех включенных преобразованиях и полифилах, добавьте поле debug в .babelrc:.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Нажмите Инструменты.
  • Нажмите Журналы.

Перезагрузите приложение и посмотрите журналы статуса Glitch в нижней части редактора.

Целевые браузеры

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.25%") и исключать определенные браузеры, которые ваши пользователи точно не используют. Чтобы узнать больше, прочитайте статью Джеймса Кайла "Last 2 versions" considered harmful (на английском языке).

Используйте <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, в этом руководстве используется более простой подход – добавление элемента скрипта модуля вручную.

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

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

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

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

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

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

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

Заключение

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