Имеет ли смысл предварительная загрузка модулей?

От автора: модули JavaScript облегчают разработку. Однако считается, что модули могут разочаровать требовательных пользователей при первом посещении, потому что многочисленным модулям требуется больше времени для загрузки.

Когда браузер загружает веб-приложение, состоящее из модулей JavaScript, он сначала загружает модули, перечисленные в HTML. Затем браузер находит операторы import в только что загруженных модулях. Он загружает необходимые модули и снова находит операторы import, указывающие на дополнительные модули для загрузки. В конце концов, после нескольких итераций загрузки и анализа браузер загружает независимые модули без операторов import.

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

Проблема в том, что браузер заранее не знает, какие модули ему придется загружать. Только после загрузки модуля браузер видит его imports. И обычно дерево зависимостей модулей начинается с единственного корневого модуля, указанного в HTML.

Тег link со значением type=»modulepreload» может использоваться, чтобы заранее сообщить браузеру, какие модули ему нужно будет загрузить. Когда все модули, составляющие приложение, объявлены в HTML, браузер немедленно начинает их загружать, не тратя время на иерархическое обнаружение и загрузку зависимостей.

Хотя Chrome анализирует предварительно загруженные модули, он предварительно не загружает обнаруженные зависимости. Поэтому для оптимальной производительности необходимо объявить все модули приложения. На самом деле проще объявить все модули, чем объявить только некоторые из них.

Чтобы продемонстрировать, как предварительная загрузка сокращает время загрузки веб-страницы, я буду использовать образец страницы no.html, которая в общей сложности загружает 255 крошечных символьных модуля. Образцы модулей пронумерованы именами, начиная с module1.js и заканчивая module255.js. Дерево зависимостей начинается с корневого module1.js, объявленного в HTML. За исключением 128 модулей, у которых нет imports, корневой модуль и каждый модуль на странице импортируют два уникальных модуля. Таким образом, дерево зависимостей выглядит как идеальное двоичное дерево с 8 уровнями. Уровни содержат 1, 2, 4, 8, 16, 32, 64 и 128 модулей. На рисунке ниже показаны только первые четыре уровня:

Имеет ли смысл предварительная загрузка модулей?

В реальных веб-приложениях модули импортируют более двух модулей. В моем примере приложения используется два импорта на один модуль, чтобы продемонстрировать проблему с сетевыми запросами. Прежде чем браузер узнает о каких-либо дочерних модулях, он сделает 7 последовательных запросов на загрузку их родителей.

Я сравниваю время загрузки no.html со временем загрузки preload.html, который является идентичной страницей, но со всеми модулями, перечисленными в links в блоке head. Поскольку имена модулей-примеров отличаются только цифрами в конце, вместо добавления тегов link в HTML, я использую цикл, который добавляет link элементы в DOM:

<!-- preload.html -->
<html>
<head> <script>const t0 = Date.now();</script> <script> for (let i = 1; i < 256; i++) { document.head.insertAdjacentHTML('beforeend', `<link rel="modulepreload" href="js/deps/module${i}.js">`) } </script>
</head>
<body> <script src="js/deps/module1.js" type="module"></script> <div id=root></div>
</body>
</html>

Чтобы сравнить время загрузки двух примеров страниц, я постепенно загружаю их в iframe другой страницы index.html. Чтобы получить более точное среднее время, каждая страница загружается в 10 iframe. Когда корневой модуль module1.js наконец запускается, он вычисляет и отображает разницу между Date.now() и t0 записанным в начале HTML:

import val2 from './module2.js';
import val3 from './module3.js';const total=Date.now()-t0;
document.querySelector('div').replaceChildren(total ); window.parent.postMessage(total,"*");

Я подробно описал код в нескольких своих предыдущих статьях, например, в статье о преимуществах HTTP2 для модульных веб-приложений, но вам не нужно знать детали кода, чтобы оценить поразительный эффект предварительной загрузки.

Важно отметить, что для стабильной имитации сценария первого посещения образцы модулей обслуживаются с заголовком cache-control: no-store, max-age=0, поэтому они не могут быть кэшированы браузером. Если вы посетите страницу примера по адресу https://modulepreload.onrender.com/, вы увидите 20 окон iframe, каждый из которых отображает миллисекунды, затраченные на его загрузку:

Имеет ли смысл предварительная загрузка модулей?

Среднее время справа. Эффект от предварительной загрузки очевиден.

В этом посте я имитировал сценарий первого посещения, предотвратив кеширование модулей. В реальной жизни, если модули страницы кэшируются при первом посещении, а пользователь возвращается на ту же страницу, модули будут загружаться из кеша браузера еще быстрее. Однако, чтобы обновлять кэшированные модули, следует использовать специальную Cache-Control директиву stale-while-revalidate.

Полный пример кода можно загрузить с https://github.com/marianc000/modulepreload.

Автор: Marian Čaikovski

Источник: marian-caikovski.medium.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен