Простая в реализации методика, избавляющая разработчиков от связывания

Модули 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 листовых модулей, у которых нет import, корневой модуль и каждый модуль на странице импортируют два уникальных модуля. Таким образом, дерево зависимостей выглядит как идеальное двоичное дерево с 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 с. Одновременно загружается только один 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