Сегодня мы публикуем вторую часть перевода материала, который посвящён использованию JS-модулей в продакшне.
→ Кстати, вот первая часть статьи.
Один из минусов использования реальных выражений импортов для разделения кода и загрузки модулей заключается в том, что задача работы с браузерами, не поддерживающими модули, ложится на разработчика.
И если вы хотите использовать динамические команды
К счастью, эту проблему нам поможет решить маленький (размером около 400 байтов) и чрезвычайно быстрый полифилл.
Добавить этот полифилл в веб-проект очень просто. Нужно лишь импортировать его и инициализировать его в главной точке входа в приложение (перед вызовом любой из имеющихся в коде команды
И последнее, что нужно сделать для того, чтобы эта схема заработала, заключается в том, чтобы сообщить Rollup о том, что ему нужно переименовать динамические команды
Причина, по которой нужно переименовывать выражения
Но очень хорошо то, что Rollup переименовывает команды во время сборки проекта, так как это означает, что в исходном коде можно использовать стандартные команды. Кроме того, в будущем, когда полифилл уже будет не нужен, исходный код проекта не придётся переписывать, меняя на
Всегда, когда вы используете разделение кода, не помешает и организовать предварительную загрузку всех модулей, которые, как вам известно, будут очень скоро загружены (например — это все модули в дереве зависимостей главного модуля, представляющего собой точку входа в проект).
Но когда мы загружаем реальные JavaScript-модули (через
На самом деле,
Это означает, что загрузка модулей с использованием атрибута
Входной фрагмент в объекте Rollup bundle содержит полный список импортов в их статических деревьях зависимостей. В результате, в хуке Rollup generateBundle, не составляет сложностей получение списка файлов, которые нуждаются в предварительной загрузке.
Хотя в npm и можно найти плагины для формирования modulepreload-списков, создание подобного списка для каждой входной точки дерева зависимостей требует лишь нескольких строк кода. Поэтому я предпочитаю создавать подобные списки вручную, с помощью примерно такого кода:
Вот, например, как я создал modulepreload-список для philipwalton.com и для моего демонстрационного приложения, о котором мы поговорим ниже.
Обратите внимание на то, что хотя атрибут
В том, что касается использования
Если вы уже используете бандлер наподобие webpack, а так же если уже применяете разделение кода на фрагменты и предварительную загрузку соответствующих файлов (примерно так, как я только что рассказывал), то вы, возможно, задаётесь вопросом о том, стоит ли вам переключаться на стратегию, ориентированную на использование реальных модулей. Есть несколько причин, которые заставляют меня полагать, что вам стоит рассмотреть возможность перехода на модули. Кроме того, я считаю, что преобразование проекта в реальные модули лучше, чем использование классических скриптов с их собственным кодом, рассчитанным на загрузку модулей.
Если в проекте используются реальные модули, то пользователям современных браузеров не придётся загружать некий дополнительный код, предназначенный для загрузки модулей или для управления зависимостями. Например, при использовании реальных модулей не нужно будет загружать механизмы времени выполнения и манифест webpack.
Как уже было сказано в предыдущем разделе, использование атрибута
В результате, вне зависимости от того, на фрагменты какого размера разделён код приложения, гораздо производительнее будет выполнять загрузку этих фрагментов с использованием команд импорта и атрибута
Другими словами, Rollup-бандл кода некоего проекта, состоящий из 20 фрагментов-модулей, будет загружаться быстрее, чем бандл того же проекта, состоящий из 20 фрагментов-классических скриптов, подготовленных средствами webpack (не из-за применения webpack, а из-за того, что это не настоящие модули).
Множество новых замечательных возможностей браузеров построено на основе модулей, а не на основе классических скриптов. Это означает, что если вы планируете использовать эти возможности, то ваш код должен быть представлен в виде настоящих модулей. Это не должно быть нечто, транспилированное в ES5 и загруженное средствами классического тега
Вот некоторые из самых интересных новых возможностей браузеров, которые ориентированы исключительно на модули:
В глобальном масштабе более 83% браузеров поддерживают JavaScript-модули (включая динамический импорт), в результате большинство пользователей сможет работать с проектом, который перешёл на модули, без каких-либо особых усилий со стороны разработчиков этого проекта.
В случае с браузерами, которые поддерживают модули, но не поддерживают динамический импорт, можно порекомендовать использовать вышеописанный полифилл dynamic-import-polyfill. Так как он очень мал и, если есть такая возможность, пользуется стандартным браузерным методом
Если же говорить о браузерах, которые совершенно не поддерживают модули, то, для организации работы с ними, можно воспользоваться паттерном module/nomodule.
Так как всегда легче говорить о кросс-браузерной совместимости, чем её добиться, я создал демонстрационное приложение, в котором используются рассмотренные выше технологии.
Это приложение работает в браузерах, вроде Edge 18 и Firefox ESR, которые не поддерживают динамические команды
Для того чтобы показать, что обсуждаемая здесь стратегия подходит не только для простых проектов, я задействовал в этом приложении множество возможностей, которые нужны в наши дни в крупных проектах:
Код проекта можно найти на GitHub (то есть — вы можете сделать форк репозитория и собрать проект самостоятельно), демо-версия хостится на Glitch, что позволяет вам с ней поэкспериментировать.
Самое важное в демонстрационном проекте — это Rollup-конфигурация, так как она определяет то, как создаются итоговые модули.
Надеюсь, этот материал убедил вас не только в возможности развёртывания стандартных JavaScript-модулей в продакшне, но и в том, что это может реально улучшить время загрузки сайта и его производительность.
Вот краткий обзор шагов, которые надо предпринять для того, чтобы внедрить модули в проект:
Если вы уже используете для сборки проекта Rollup — мне хотелось бы, чтобы вы попробовали то, о чём я тут рассказывал, и вышли бы на развёртывание в продакшне реальных модулей (с использованием методик разделения кода и динамического импорта). Если вы так и сделаете — дайте мне знать о том, как у вас идут дела. Мне интересно знать и о проблемах, и об удачных случаях внедрения модулей.
Совершенно очевидно то, что модули — это будущее JavaScript. Мне хотелось бы увидеть, и желательно — поскорее, как используемые нами инструменты и вспомогательные библиотеки принимают эту технологию. Надеюсь, данный материал сможет хотя бы немного помочь этому процессу.
Уважаемые читатели! Пользуетесь ли вы JS-модулями в продакшне?
→ Кстати, вот первая часть статьи.
Динамический импорт
Один из минусов использования реальных выражений импортов для разделения кода и загрузки модулей заключается в том, что задача работы с браузерами, не поддерживающими модули, ложится на разработчика.
И если вы хотите использовать динамические команды
import()
для организации ленивой загрузки кода, тогда вам, кроме прочего, придётся иметь дело с тем фактом, что некоторые браузеры, хотя, совершенно точно, поддерживают модули, всё же не поддерживают динамические команды import() (Edge 16–18, Firefox 60–66, Safari 11, Chrome 61–63).К счастью, эту проблему нам поможет решить маленький (размером около 400 байтов) и чрезвычайно быстрый полифилл.
Добавить этот полифилл в веб-проект очень просто. Нужно лишь импортировать его и инициализировать его в главной точке входа в приложение (перед вызовом любой из имеющихся в коде команды
import()
):import dynamicImportPolyfill from 'dynamic-import-polyfill';
// Это нужно сделать до использования любых команд динамического импорта. Если ваши
// модули располагаются в поддиректории, то здесь нужно указать путь к ним.
dynamicImportPolyfill.initialize({modulePath: '/modules/'});
И последнее, что нужно сделать для того, чтобы эта схема заработала, заключается в том, чтобы сообщить Rollup о том, что ему нужно переименовать динамические команды
import()
, встречающиеся в коде, используя выбранное вами имя (через опцию output.dynamicImportFunction). Полифилл, реализующий возможность динамического импорта, использует по умолчанию имя __import__
, но оно поддаётся настройке.Причина, по которой нужно переименовывать выражения
import()
, заключается в том, что import
— это, в JavaScript, ключевое слово. Это означает, что невозможно, средствами полифилла, организовать замену стандартной команды import()
на команду с тем же именем. Если попытаться это сделать — произойдёт синтаксическая ошибка.Но очень хорошо то, что Rollup переименовывает команды во время сборки проекта, так как это означает, что в исходном коде можно использовать стандартные команды. Кроме того, в будущем, когда полифилл уже будет не нужен, исходный код проекта не придётся переписывать, меняя на
import
то, что раньше было названо как-то иначе.Эффективная загрузка JavaScript-модулей
Всегда, когда вы используете разделение кода, не помешает и организовать предварительную загрузку всех модулей, которые, как вам известно, будут очень скоро загружены (например — это все модули в дереве зависимостей главного модуля, представляющего собой точку входа в проект).
Но когда мы загружаем реальные JavaScript-модули (через
<script type="module">
и затем — через соответствующие команды import
), надо использовать атрибут modulepreload вместо обычного preload, который предназначен лишь для классических скриптов.<link rel="modulepreload" href="/modules/main.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-one.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-two.XXXX.mjs">
<link rel="modulepreload" href="/modules/npm.pkg-three.XXXX.mjs">
<!-- ... -->
<script type="module" src="/modules/main.XXXX.mjs"></script>
На самом деле,
modulepreload
, определённо, лучше традиционного механизма preload
в деле организации предварительной загрузки настоящих модулей. Дело в том, что при его использовании не просто выполняется загрузка файла. Он, кроме того, немедленно, за пределами главного потока, приступает к парсингу и компиляции скрипта. Обычный preload
этого делать не может из-за того, что он, во время предварительной загрузки, не знает о том, будет ли файл использоваться как модуль или как обычный скрипт.Это означает, что загрузка модулей с использованием атрибута
modulepreload
часто происходит быстрее, и то, что при инициализации модулей менее вероятно создание чрезмерной нагрузки на главный поток, вызывающей проблемы с интерфейсом.Создание списка модулей для предварительной загрузки
Входной фрагмент в объекте Rollup bundle содержит полный список импортов в их статических деревьях зависимостей. В результате, в хуке Rollup generateBundle, не составляет сложностей получение списка файлов, которые нуждаются в предварительной загрузке.
Хотя в npm и можно найти плагины для формирования modulepreload-списков, создание подобного списка для каждой входной точки дерева зависимостей требует лишь нескольких строк кода. Поэтому я предпочитаю создавать подобные списки вручную, с помощью примерно такого кода:
{
generateBundle(options, bundle) {
// Мэппинг имён входных фрагментов на полные списки их зависимостей.
const modulepreloadMap = {};
for (const [fileName, chunkInfo] of Object.entries(bundle)) {
if (chunkInfo.isEntry || chunkInfo.isDynamicEntry) {
modulepreloadMap[chunkInfo.name] = [fileName, ...chunkInfo.imports];
}
}
// Делаем что-то с результатами мэппинга...
console.log(modulepreloadMap);
}
}
Вот, например, как я создал modulepreload-список для philipwalton.com и для моего демонстрационного приложения, о котором мы поговорим ниже.
Обратите внимание на то, что хотя атрибут
modulepreload
определённо лучше классического preload
для загрузки скриптов модулей, он отличается худшей браузерной поддержкой (в настоящий момент его поддерживает лишь Chrome). Если заметная порция вашего трафика приходит не из Chrome, то в вашей ситуации может иметь смысл использовать вместо modulepreload
обычный preload
.В том, что касается использования
preload
, мне хотелось бы вас кое о чём предупредить. Дело в том, что при загрузке скриптов с помощью preload
, в отличие от modulepreload
, эти скрипты не попадают в карту модулей браузера. Это означает, что существует возможность того, что preload-запросы могут быть выполнены более одного раза (например, если модуль импортирует файл до того, как браузер завершил его предварительную загрузку).Зачем развёртывать в продакшне реальные модули?
Если вы уже используете бандлер наподобие webpack, а так же если уже применяете разделение кода на фрагменты и предварительную загрузку соответствующих файлов (примерно так, как я только что рассказывал), то вы, возможно, задаётесь вопросом о том, стоит ли вам переключаться на стратегию, ориентированную на использование реальных модулей. Есть несколько причин, которые заставляют меня полагать, что вам стоит рассмотреть возможность перехода на модули. Кроме того, я считаю, что преобразование проекта в реальные модули лучше, чем использование классических скриптов с их собственным кодом, рассчитанным на загрузку модулей.
▍Уменьшение общего объёма кода
Если в проекте используются реальные модули, то пользователям современных браузеров не придётся загружать некий дополнительный код, предназначенный для загрузки модулей или для управления зависимостями. Например, при использовании реальных модулей не нужно будет загружать механизмы времени выполнения и манифест webpack.
▍Улучшение предварительной загрузки кода
Как уже было сказано в предыдущем разделе, использование атрибута
modulepreload
позволяет загружать код и выполнять его парсинг и компиляцию за пределами главного потока. Всё остальное, в сравнении с атрибутом preload
, остаётся тем же самым. Это означает, что благодаря modulepreload
страницы быстрее становятся интерактивными, и то, что снижается вероятность блокировки главного потока в ходе взаимодействия с пользователем.В результате, вне зависимости от того, на фрагменты какого размера разделён код приложения, гораздо производительнее будет выполнять загрузку этих фрагментов с использованием команд импорта и атрибута
modulepreload
, чем загружать их, пользуясь обычным тегом script
и обычным атрибутом preload
(особенно — если соответствующие теги генерируются динамически и добавляются в DOM во время выполнения программы).Другими словами, Rollup-бандл кода некоего проекта, состоящий из 20 фрагментов-модулей, будет загружаться быстрее, чем бандл того же проекта, состоящий из 20 фрагментов-классических скриптов, подготовленных средствами webpack (не из-за применения webpack, а из-за того, что это не настоящие модули).
▍Улучшение ориентированности кода на будущее
Множество новых замечательных возможностей браузеров построено на основе модулей, а не на основе классических скриптов. Это означает, что если вы планируете использовать эти возможности, то ваш код должен быть представлен в виде настоящих модулей. Это не должно быть нечто, транспилированное в ES5 и загруженное средствами классического тега
script
(это — проблема, о которой я писал, когда пытался воспользоваться экспериментальным API KV Storage).Вот некоторые из самых интересных новых возможностей браузеров, которые ориентированы исключительно на модули:
- Встроенные модули
- HTML-модули
- CSS-модули
- JSON-модули
- Карты импорта
- Совместное использование модулей воркерами, сервис-воркерами и окном документа
Поддержка устаревших браузеров
В глобальном масштабе более 83% браузеров поддерживают JavaScript-модули (включая динамический импорт), в результате большинство пользователей сможет работать с проектом, который перешёл на модули, без каких-либо особых усилий со стороны разработчиков этого проекта.
В случае с браузерами, которые поддерживают модули, но не поддерживают динамический импорт, можно порекомендовать использовать вышеописанный полифилл dynamic-import-polyfill. Так как он очень мал и, если есть такая возможность, пользуется стандартным браузерным методом
import()
, применение этого полифилла практически никак не влияет на размер или производительность проекта.Если же говорить о браузерах, которые совершенно не поддерживают модули, то, для организации работы с ними, можно воспользоваться паттерном module/nomodule.
▍Рабочий пример
Так как всегда легче говорить о кросс-браузерной совместимости, чем её добиться, я создал демонстрационное приложение, в котором используются рассмотренные выше технологии.
Это приложение работает в браузерах, вроде Edge 18 и Firefox ESR, которые не поддерживают динамические команды
import()
. Кроме того, оно работает и в браузерах, наподобие Internet Explorer 11, которые не поддерживают модули.Для того чтобы показать, что обсуждаемая здесь стратегия подходит не только для простых проектов, я задействовал в этом приложении множество возможностей, которые нужны в наши дни в крупных проектах:
- Трансформация кода с использованием Babel (включая JSX).
- CommonJS-зависимости (например — react и react-dom).
- CSS-зависимости.
- Хэширование ресурсов.
- Разделение кода.
- Динамический импорт (с запасным вариантом в виде полифилла).
- Реализация паттерна module/nomodule.
Код проекта можно найти на GitHub (то есть — вы можете сделать форк репозитория и собрать проект самостоятельно), демо-версия хостится на Glitch, что позволяет вам с ней поэкспериментировать.
Самое важное в демонстрационном проекте — это Rollup-конфигурация, так как она определяет то, как создаются итоговые модули.
Итоги
Надеюсь, этот материал убедил вас не только в возможности развёртывания стандартных JavaScript-модулей в продакшне, но и в том, что это может реально улучшить время загрузки сайта и его производительность.
Вот краткий обзор шагов, которые надо предпринять для того, чтобы внедрить модули в проект:
- Используйте бандлер, среди выходных форматов, поддерживаемых которым, есть ES2015-модули.
- Агрессивно подходите к разделению кода (если возможно — то вплоть до выделения в отдельные фрагменты зависимостей из
node_modules
). - Выполняйте предварительную загрузку всех модулей, которые имеются в вашем статическом дереве зависимостей (с использованием
modulepreload
). - Используйте полифилл для работы с браузерами, которые не поддерживают динамические инструкции
import()
. - Используйте паттерн module/nomodule для организации работы с браузерами, которые не поддерживают модули.
Если вы уже используете для сборки проекта Rollup — мне хотелось бы, чтобы вы попробовали то, о чём я тут рассказывал, и вышли бы на развёртывание в продакшне реальных модулей (с использованием методик разделения кода и динамического импорта). Если вы так и сделаете — дайте мне знать о том, как у вас идут дела. Мне интересно знать и о проблемах, и об удачных случаях внедрения модулей.
Совершенно очевидно то, что модули — это будущее JavaScript. Мне хотелось бы увидеть, и желательно — поскорее, как используемые нами инструменты и вспомогательные библиотеки принимают эту технологию. Надеюсь, данный материал сможет хотя бы немного помочь этому процессу.
Уважаемые читатели! Пользуетесь ли вы JS-модулями в продакшне?