- PVSM.RU - https://www.pvsm.ru -

Развертывание кода ES2015+ в продакшн сегодня

Большинство веб-разработчиков, с которыми я общаюсь сейчас, любят писать JavaScript со всеми новейшими функциями языка — async/await, классами, стрелочными функциями и т.д. Однако, несмотря на то, что все современные браузеры могут исполнять код ES2015+ и изначально поддерживают упомянутый мной функционал, большинство разработчиков по-прежнему транспилируют свой код на ES5 и связывают его с полифиллами, чтобы удовлетворить небольшой процент пользователей, все еще работающих в старых браузерах.

Это отвратительно. В идеальном мире мы не будем развертывать ненужный код.
Развертывание кода ES2015+ в продакшн сегодня - 1

При работе с новыми API-интерфейсами JavaScript и DOM мы можем условно загружать полифиллы [1], т.к. мы можем выявить поддержку этих интерфейсов во время выполнения программы. Но с новым синтаксисом JavaScript сделать это намного сложнее, поскольку любой неизвестный синтаксис вызовет ошибку синтаксического анализа (parse error), и тогда наш код вообще не будет запущен.

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

Решим это с помощью тега script type="module".

Большинство разработчиков думают о script type="module" как о способе загрузки модулей ES (и, конечно же, это так), но script type="module" также имеет более быстрый и практичный вариант использования — загружает обычные файлы JavaScript с функциями ES2015+, зная, что браузер может справиться с ними!

Другими словами, каждый браузер, поддерживающий script type="module" также поддерживает большинство функций ES2015+, которые вы знаете и любите. Например:

  • Каждый браузер, поддерживающий script type="module", также поддерживает async/await
  • Каждый браузер, поддерживающий script type="module", также поддерживает классы.
  • Каждый браузер, поддерживающий script type="module", также поддерживает стрелочные функции.
  • Каждый браузер, поддерживающий script type="module", также поддерживает fetch, Promises, Map, Set, и многое другое!

Осталось только предоставить резервную копию для браузеров, которые не поддерживают script type="module". К счастью, если вы в настоящее время генерируете ES5-версию своего кода, вы уже сделали эту работу. Все, что вам теперь нужно — создать версию ES2015+!

В остальной части этой статьи объясняется, как реализовать эту технику, и обсуждается, как возможность развертывания кода ES2015+ изменит способ создания модулей в будущем.

Реализация

Если вы уже используете сборщик модулей (module bundler), например webpack или rollup для генерации своего кода на JavaScript, продолжайте по-прежнему это делать.

Затем, в дополнение к вашему текущему набору (bundle), вы создадите второй набор, как и первый; единственное различие будет заключается в том, что вы не будете транспилировать код в ES5, и вам не нужно будет подключать устаревшие полифиллы (legacy polyfills).

Если вы уже используете babel-preset-env [2] (что должны), то второй шаг будет очень прост. Все, что вам нужно сделать, это изменить список браузеров только на те, которые поддерживают script type="module", и Babel автоматически не будет делать ненужные преобразования.

Иными словами, это будет вывод кода ES2015+ вместо ES5.

Например, если вы используете webpack, и вашей основной точкой входа является скрипт ./path/to/main.js, тогда конфигурация вашей текущей версии ES5 может иметь следующий вид (обратите внимание, так как это ES5, я называю набор (bundle) main-legacy):

module.exports = {
  entry: {
    'main-legacy': './path/to/main.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  '> 1%',
                  'last 2 versions',
                  'Firefox ESR',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

Для того, чтобы сделать современную версию для ES2015+, все, что вам нужно — это создать вторую конфигурацию и настроить целевую среду только для браузеров, поддерживающих script type="module". Вот как это может выглядеть:

module.exports = {
  entry: {
    'main': './path/to/main.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [{
      test: /.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['env', {
              modules: false,
              useBuiltIns: true,
              targets: {
                browsers: [
                  'Chrome >= 60',
                  'Safari >= 10.1',
                  'iOS >= 10.3',
                  'Firefox >= 54',
                  'Edge >= 15',
                ],
              },
            }],
          ],
        },
      },
    }],
  },
};

При запуске эти две конфигурации выведут на продакшн два JavaScript-файла:

  • main.js (ES2015+ синтаксис)
  • main-legacy.js (ES5 синтаксис)

Следующим шагом будет обновление вашего HTML для условной загрузки ES2015+ пакета (bundle) в браузерах, поддерживающих модули. Вы можете сделать это, используя script type="module" и script nomodule:

<!-- Браузеры с поддержкой модулей ES загрузят этот файл. -->
<script type="module" src="main.js"></script>

<!-- Устаревшие браузеры загрузят этот файл (поддерживающие модули -->
<!-- браузеры знают, что этот файл загружать *не* нужно). -->
<script nomodule src="main-legacy.js"></script>

Внимание! Единственная засада (gotcha) здесь — браузер Safari 10, который не поддерживает атрибут nomodule, но вы можете решить это, встроив JavaScript-сниппет [3] в ваш HTML до использования любых тегов script nomodule. (Примечание: это было исправлено в Safari 11).

Важные моменты

По большей части описанная выше техника «просто работает», но перед ее реализацией важно знать несколько деталей о том, как загружаются модули:

  1. Модули загружаются как script defer. Это означает, что они не выполняются до тех пор, пока документ не будет распарсен. Если какую-то часть вашего кода нужно запустить раньше, лучше разбить этот код и загрузить его отдельно.
  2. Модули всегда запускают код в строгом режиме [4] (strict mode), поэтому, если по какой-либо причине часть вашего кода должна быть запущена за пределами строгого режима, ее придется загружать отдельно.
  3. Модули обрабатывают объявления верхнего уровня переменных (var) и функций (function) отлично от обычных сценариев. Например, к var foo = 'bar' и function foo() {…} в скрипте можно получить доступ через window.foo, но в модуле это не будет работать. Убедитесь, что в своем коде вы не зависите от такого поведения.

Рабочий пример

Я создал webpack-esnext-boilerplate [5], чтобы разработчики могли увидеть реальное применение описанной здесь техники.

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

  • Разделение кода (Code splitting [6])
  • Динамический импорт (Dynamic imports [7], условная загрузка дополнительного кода во время выполнения программы)
  • Asset fingerprinting [8] (для эффективного длительного кэширования)

И так как я никогда не буду рекомендовать то, что не использую сам, я обновил свой блог, чтобы использовать эту технику. Вы можете проверить исходный код [9], если хотите увидеть больше.

Если для создания пакетов (bundles) продакшн вы используете инструмент, отличный от webpack, этот процесс более или менее одинаковый. Я решил продемонстрировать описанную мной технику с помощью webpack, поскольку в настоящее время он является самым популярным инструментом для сборки, а также и самым сложным. Я полагаю, что если описанная мной техника может работать с webpack, она может работать с чем угодно.

Игра стоит свеч?

По-моему, определенно! Экономия может быть значительной. Например, ниже приведено сравнение общих размеров файлов для двух версий кода из моего блога:

Версия Размер (minified) Размер (minified + gzipped)
ES2015+ (main.js) 80K 21K
ES5 (main-legacy.js) 175K 43K

Устаревшая ES5-версия кода более чем в два раза превышает размер (даже gzipped) версии ES2015+.

Большие файлы занимают больше времени для загрузки, но они также занимают больше времени для анализа и оценки. При сравнении двух версий моего блога, время, затраченное на parse/eval, также было стабильно вдвое дольше для устаревшей ES5-версии (эти тесты выполнялись на Moto G4 с использованием webpagetest.org [10]):

Версия Parse/eval time (по отдельности) Parse/eval time (среднее)
ES2015+ (main.js) 184ms, 164ms, 166ms 172ms
ES5 (main-legacy.js) 389ms, 351ms, 360ms 367ms

Хотя эти абсолютные размеры файлов и время parse/eval не особенно большие, поймите, что это блог, и я не загружаю много скриптов. Но для большинства сайтов это не так. Чем больше у вас скриптов, тем больше будет выигрыш, который вы получите, развернув код на ES2015+ в своем проекте.

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

Быстрый запрос данных HTTPArchive [11] показывает, что из лучших сайтов по рейтингу Alexa, 85 181 включают в себя babel-polyfill [12], core-js [13] или regenerator-runtime [14] в своих пакетах (bundles) продакшн. Шесть месяцев назад их число было 34 588!

Реальность транспилируется, и в том числе полифиллы быстро становятся новой нормой. К сожалению, это означает, что миллиарды пользователей получают триллионы байтов, без необходимости отправленные через сеть в браузеры, которые изначально были бы вполне способны запускать нетранспилированный код.

Пришло время собирать наши модули как ES2015

Главная засада (gotcha) для описанной здесь техники сейчас состоит в том, что большинство авторов модулей не публикуют ES2015+ версии исходного кода, а публикуют сразу транспилированную ES5-версию.

Теперь, когда развертывание кода на ES2015+ возможно, пришло время изменить это.

Я полностью осознаю, что такой шаг сопряжен с множеством проблем в ближайшем будущем. Сегодня большинство инструментов сборки публикуют документацию, рекомендующую конфигурацию [15], которая предполагает [16], что все модули написаны на ES5. Это означает, что если авторы модулей начнут публиковать исходный код на ES2015+ в npm, то они, вероятно, сломают [17] некоторые сборки пользователей и просто вызовут путаницу.

Проблема заключается в том, что большинство использующих Babel разработчиков настраивают его так, чтобы код в node_modules не транспилировался. Однако, если модули опубликованы с исходным кодом ES2015+, возникает проблема. К счастью, она легко исправима. Вам просто нужно удалить исключение node_modules из конфигурации сборки:

rules: [
  {
    test: /.js$/,
    exclude: /node_modules/, // удалите эту строку
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['env']
      }
    }
  }
]

Недостаток заключается в том, что если такие инструменты как Babel должны начать транспилировать зависимости (dependencies) из node_modules, в дополнение к локальным зависимостям, это замедлит скорость сборки. К счастью, эту проблему можно отчасти решить на уровне инструментария [18] с постоянным локальным кэшированием.

Несмотря на удары, мы, скорее всего, пройдем путь к тому, что ES2015+ станет новым стандартом публикации модулей. Я думаю, что борьба стоит своей цели. Если мы, как авторы модулей, публикуем в npm только ES5-версии нашего кода, мы навязываем пользователям раздутый (bloated) и медленный код.

Публикуя код в ES2015, мы даем разработчикам выбор, что в конечном счете приносит пользу всем.

Заключение

Хотя script type="module" предназначен для загрузки модулей ES (и их зависимостей) в браузере, его не нужно использовать только для этой цели.

script type="module" будет успешно загружать единственный файл Javascript, и это даст разработчикам столь необходимое средство для условной загрузки современного функционала в тех браузерах, которые могут его поддерживать.

Это, наряду с атрибутом nomodule, дает нам возможность использовать код ES2015+ в продакшн, и наконец-то мы можем прекратить отправку транспилированного кода в браузеры, которые в нем не нуждаются.

Написание кода ES2015 — это победа для разработчиков, а внедрение кода ES2015 — победа для пользователей.

Автор: Олег Лазарев

Источник [19]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/264350

Ссылки в тексте:

[1] условно загружать полифиллы: https://philipwalton.com/articles/loading-polyfills-only-when-needed/

[2] babel-preset-env: https://github.com/babel/babel-preset-env

[3] встроив JavaScript-сниппет: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc

[4] строгом режиме: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Strict_mode

[5] webpack-esnext-boilerplate: https://github.com/philipwalton/webpack-esnext-boilerplate

[6] Code splitting: https://webpack.js.org/guides/code-splitting/

[7] Dynamic imports: https://webpack.js.org/guides/code-splitting/#dynamic-imports

[8] Asset fingerprinting: https://webpack.js.org/guides/caching/

[9] исходный код: https://github.com/philipwalton/blog

[10] webpagetest.org: https://webpagetest.org/

[11] запрос данных HTTPArchive: https://bigquery.cloud.google.com/savedquery/438218511550:2cee796ae27f472fbfd517606a4bafc3

[12] babel-polyfill: https://babeljs.io/docs/usage/polyfill/

[13] core-js: https://github.com/zloirock/core-js

[14] regenerator-runtime: https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js

[15] рекомендующую конфигурацию: https://github.com/babel/babel-loader/blob/v7.1.2/README.md#usage

[16] предполагает: https://rollupjs.org/#babel

[17] сломают: https://github.com/googleanalytics/autotrack/issues/137

[18] на уровне инструментария: https://github.com/babel/babel-loader/blob/v7.1.2/README.md#babel-loader-is-slow

[19] Источник: https://habrahabr.ru/post/338612/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best