Спекулятивное получение модулей

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

Цель Qwik не в том, чтобы предварительно заполнить кэш приложением целиком, а в том, чтобы кэшировать то, что возможно на данный момент. Когда оптимизатор Qwik разбивает приложение на фрагменты, он способен понять возможные пути взаимодействия с пользователем. И исходя из этого, не менее важно, чтобы он мог понять, что невозможно из взаимодействия с пользователем, и избежать загрузки этих бандлов.

Предварительное заполнение кэша страницы

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

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

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

Событие предварительного заполнения кэша

Рекомендуемая стратегия предварительной загрузки заключается в использовании сервис-воркера для наполнения кэша браузера. Qwik должен быть настроен на использование реализации prefetchEvent, которая используется по умолчанию.

Предварительное заполнение кэша с помощью сервис-воркера

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

Qwik City, однако, использует сервис-воркер совершенно иначе, чтобы обеспечить мощную стратегию кэширования. Целью Qwik является не загрузка всего приложения, а использование сервис-воркера для динамической предварительной загрузки того, что возможно выполнить. Благодаря загрузке не всего приложения, ресурсы освобождаются, позволяя пользователю запрашивать только необходимые части приложения, которые он может использовать для выполнения текущей задачи на экране.

Кроме того, сервис-воркер автоматически добавит слушателей для этих событий, создаваемых Qwik.

Фоновая задача

Преимущество использования сервис-воркера заключается в том, что он является расширением воркера, который выполняется в фоновом потоке.

Web-воркер позволяет выполнять операции скрипта в фоновом потоке, отдельном от основного потока выполнения веб-приложения. Преимуществом этого является то, что трудоёмкая обработка может быть выполнена в отдельном потоке, позволяя основному (обычно UI) потоку работать без блокировки/замедления.

Предварительное заполнение кэша сервис-воркером (который является наследником воркера) позволяет запустить код в фоновом режиме, чтобы не мешать основному потоку пользовательского интерфейса. Не вмешиваясь в основной поток пользовательского интерфейса, мы можем улучшить производительность приложения Qwik для пользователей.

Интерактивное предварительное заполнение кэша

Qwik должен быть настроен на использование стратегии prefetchEvent (которая используется в Qwik по умолчанию). Когда Qwik инициирует событие, данные события активно пересылаются установленному и активному сервис-воркеру.

Затем сервис-воркер (который запускается в фоновом потоке) получает модули и добавляет их в кэш браузера. Главному потоку нужно только сообщать о необходимых бандлах, в то время как единственной задачей сервис-воркера является их кэширование.

  1. Браузер уже кэшировал этот фрагмент? Отлично, ничего не делаем!
  2. Если браузер еще не кэшировал этот фрагмент, давайте запустим запрос на получение.

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

Кэширование пар запросов и ответов

Во многих традиционных фреймворках предпочтительной стратегией является использование HTML-тэга <link> с атрибутом rel и значением prefetch, preload или modulepreload. Однако есть достаточно известные проблемы, из-за которых Qwik предпочел не делать link стратегией предварительной загрузки по умолчанию (хотя её всё ещё можно настроить).

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

API кэша

API кэша часто ассоциируемое с сервис-воркерами, как способ хранения пар запросов и ответов для того, чтобы приложение могло работать в автономном режиме. Помимо возможности работы приложений без подключения, тот же Cache API предоставляет чрезвычайно мощный механизм предварительной загрузки и кэширования, доступный в Qwik.

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

Преимуществом оптимизатора Qwik является то, что он также генерирует файл q-manifest.json. Манифест предоставляет подробный граф модулей не только того, как связаны бандлы, но и того, какие ключи-символы находятся внутри каждого бандла. Эти же данные графа модулей предоставляются сервис-воркеру, что позволяет кэшировать каждый сетевой запрос для известных бандлов.

Динамические импорты и кэширование

Когда Qwik запрашивает модуль, он использует динамический import(). Например, предположим, что произошло взаимодействие с пользователем, требующее, чтобы Qwik выполнил динамический импорт для /build/q-abc.js . Код для этого будет выглядеть примерно так:

const module = await import('/build/q-abc.js');

Здесь важно то, что сам Qwik не знает о стратегии предварительной загрузки или кэширования. Это просто запрос URL-адреса. Однако, поскольку мы установили сервис-воркер, и сервис-воркер перехватывает запросы, он может проверить URL и сказать: "смотрите, это запрос на /build/q-abc.js! Это один из наших пакетов! Давайте сначала проверим, есть ли он уже в кэше, прежде чем выполнять фактический сетевой запрос."

Вот где проявляется мощь сервис-воркера и API кэша! Qwik предварительно заполняет кэш для модулей, которые пользователь вскоре может запросить в другом потоке. И ещё лучше, если он уже кэширован, тогда браузеру не нужно ничего делать.

Распараллеливание сетевых запросов

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

Избегание дублирования запросов

В качестве примера, допустим, конечный пользователь в настоящее время имеет медленное 3G-соединение. Когда он впервые запрашивает страницу лэндинга, устройство, насколько позволяет медленная сеть, загружает HTML и отображает содержимое (та область, где Qwik действительно хорош). При таком медленном соединении было бы обидно, если бы ему пришлось загружать ещё несколько сотен килобайт только для того, чтобы приложение заработало и стало интерактивным.

Однако, поскольку приложение было создано с помощью Qwik, конечному пользователю не нужно загружать всё приложение, чтобы оно стало интерактивным. Вместо этого, конечный пользователь уже загрузил HTML приложение, отрендеренное на сервере, и все интерактивные части, такие как кнопка "Добавить в корзину", могут сразу же начать свою предварительную загрузку.

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

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

Для демонстрации предположим, что предварительная загрузка для этого бандла занимает две секунды. Однако, через секунду после просмотра страницы пользователь нажимает на кнопку. В традиционном фреймворке, скорее всего, ничего не произойдёт! Если фреймворк ещё не закончил загрузку, гидратацию и ререндер, слушатель событий ещё не может быть добавлен к кнопке. Что, в свою очередь, означает, что взаимодействие пользователя будет просто потеряно.

Однако, с кэшированием Qwik, если пользователь нажал на кнопку, а мы уже начали запрос одну секунду назад, и до его полного получения осталась одна секунда, то конечному пользователю придётся ждать всего одну секунду. Вспоним, что он пользуется медленным 3G-соединением в этой демонстрации. К счастью, пользователь уже получил полную страницу лэндинга, поэтому он уже смотрит на готовую страницу. Далее, фреймворк предварительно загружает только те части приложения, с которыми пользователь может взаимодействовать, и его медленное соединение предназначено только для этих бандлов. Это контрастирует с их медленным соединением, загружающим всё приложение, только для выполнения одного слушателя.

Qwik способен перехватывать запросы на известные ему бандлы, и если предварительная загрузка уже находится в процессе, а затем пользователь запрашивает тот же пакет, то он гарантирует, что второй запрос сможет повторно использовать первый, который, возможно, уже закончил загрузку. Выполнение всего этого с помощью link невозможно, и это показывает, почему Qwik предпочёл не использовать его по умолчанию, а стал вместо этого использовать API кэширования и перехватывает запросы с помощью сервис-воркера.

Уменьшение сетевых водопадов

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

Ниже приведен пример с тремя модулями: A, B и C. Модуль A импортирует B, а B импортирует C. HTML-документ - это то, с чего начинается водопад: сначала запрашивается модуль A.

import './b.js';
console.log('Module A');
import './c.js';
console.log('Module B');
console.log('Module C');
<script type="module" src="./a.js"></script>

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

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

Пользовательский код сервис-воркера

Установленный сервис-воркер по-прежнему полностью контролируется приложением. Например, исходный файл src/routes/service-worker.ts становится /service-worker.js, который и является скриптом, запрашиваемым браузером. Обратите внимание, что его местоположение в src/routes все ещё следует той же схеме маршрутизации, основанной на каталогах.

Ниже приведён пример исходного файла по умолчанию src/routes/service-worker.ts:

import { setupServiceWorker } from '@builder.io/qwik-city/service-worker';
 
setupServiceWorker();
 
addEventListener('install', () => self.skipWaiting());
 
addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));

Исходный код src/routes/service-worker.ts может быть изменён разработчиком по своему усмотрению. Это включает в себя согласие или отказ от настройки сервис-воркера Qwik City.

Обратите внимание, что функция setupServiceWorker() импортируется из @builder.io/qwik-city/service-worker и выполняется в верхней части исходного файла. Разработчик может решить сам, когда и где вызывать эту функцию. Например, разработчик может захотеть сначала обработать какие-либо запросы на загрузку данных, и в этом случае он добавит свою собственную функцию выше, чем setupServiceWorker(). Или, если он не хочет использовать сервис-воркер Qwik City, он может просто удалить setupServiceWorker() из файла.

Кроме того, файл src/routes/service-worker.ts по умолчанию поставляется со слушателями событий установка и активация, каждый из которых добавлен в нижней части файла. Предоставленные функции обратного вызова являются рекомендуемыми функциями. Однако разработчик может изменять их в зависимости от требований своего приложения.

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

Таким образом, хотя Qwik City действительно предоставляет способ предварительной загрузки и кэширования пакетов, он не забирает полностью контроль над сервис-воркером приложения. Это позволяет разработчикам добавлять свою логику в сервис-воркер, не вступая в конфликт с Qwik.

Отключен во время разработки и предварительного просмотра

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

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

HTTP-кэш и кэш сервис-воркера

Может показаться, что спекулятивная выборка модулей не работает частично из-за различных уровней кэширования. Например, сам браузер может кэшировать запросы в своем HTTP-кэше, а сервис-воркер может кэшировать запросы в Cache API. Простой очистки одного из кэшей может быть недостаточно, чтобы увидеть эффект спекулятивной выборки модулей.

Вводящие в заблуждение пустой кэш и жёсткая перезагрузка

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

Кроме того, когда используется "Очистка кэша и жёсткая перезагрузка", браузер посылает заголовок cache-control со значеним no-cache в запросе к серверу. Поскольку запрос имеет заголовок cache-control со значеним no-cache, сервис-воркер намеренно не использует свой собственный кэш, и вместо этого браузер снова выполняет обычную HTTP-выборку.

Очистка кэша сервис-воркеров

Рекомендуемый способ тестирования спекулятивной выборки модулей следующий:

  • Удалить сервис-воркер: В Chrome DevTools перейдите на вкладку Application и в разделе Service Workers нажмите кнопку "Unregister" для сервис-воркера вашего сайта;
  • Удалите кэш состояния "QwikBuild": В Chrome DevTools перейдите на вкладку Application и в разделе Cache Storage слева, нажмите правой кнопкой мыши на кэше состояния "QwikBuild" и выберите пункт "Удалить";
  • Не делайте жёсткую перезагрузку: Вместо жёсткой перезагрузки, которая отправит сервис-воркеру cache-control со значеним no-cache, просто кликните на строке URL и нажмите клавишу Enter. Это вызовет повторный обычный запрос, как если бы вы были первым посетителем.

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

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

  • ulic75
  • mhevery
  • adamdbradley
  • hamatoyogi
  • manucorporat
  • mrhoodz
  • thejackshelton
  • zanettin
  • wtlin1228
  • aendel