Ускорьте работу сервис-воркера с помощью предварительной загрузки навигации

Предварительная загрузка навигации позволяет сократить время запуска Service Worker за счет параллельного выполнения запросов.

Поддержка браузера

  • Хром: 59.
  • Край: 18.
  • Фаерфокс: 99.
  • Сафари: 15.4.

Источник

Краткое содержание

Проблема

Когда вы переходите на сайт, который использует сервис-воркера для обработки событий выборки, браузер запрашивает у сервис-воркера ответ. Это включает в себя загрузку сервисного работника (если он еще не запущен) и отправку события выборки.

Время загрузки зависит от устройства и условий. Обычно это около 50 мс. На мобильном телефоне это больше похоже на 250 мс. В крайних случаях (медленные устройства, сбой в работе процессора) оно может превышать 500 мс. Однако, поскольку работник службы бодрствует в течение определенного браузером времени между событиями, такая задержка возникает только изредка, например, когда пользователь переходит на ваш сайт с новой вкладки или другого сайта.

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

загрузка ПО
Запрос навигации

Сетевой запрос задерживается из-за загрузки сервисного работника.

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

Facebook обратил наше внимание на влияние этой проблемы и попросил указать способ параллельного выполнения навигационных запросов:

загрузка ПО
Запрос навигации

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

Предварительная загрузка навигации — это функция, которая позволяет вам сказать: «Когда пользователь отправляет запрос навигации GET, запустите сетевой запрос во время загрузки сервисного работника».

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

Вот видео этого в действии, где сервисному работнику преднамеренно задается задержка запуска в 500 мс с использованием цикла while:

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

Активировать предварительную загрузку навигации

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Enable navigation preloads!
      await self.registration.navigationPreload.enable();
    }
  }());
});

Вы можете вызвать navigationPreload.enable() в любое время или отключить его с помощью navigationPreload.disable() . Однако, поскольку ваше событие fetch должно использовать его, лучше всего включать и отключать его в событии activate вашего сервис-воркера.

Использование предварительно загруженного ответа

Теперь браузер будет выполнять предварительную загрузку для навигации, но вам все равно придется использовать ответ:

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

event.preloadResponse — это обещание, которое разрешается с ответом, если:

  • Предварительная загрузка навигации включена.
  • Запрос представляет собой запрос GET .
  • Запрос представляет собой запрос навигации (который браузеры генерируют при загрузке страниц, включая iframe).

В противном случае event.preloadResponse все еще существует, но разрешается с помощью undefined .

Если вашей странице нужны данные из сети, самый быстрый способ — запросить их в сервис-воркере и создать единый потоковый ответ, содержащий части из кеша и части из сети.

Допустим, мы хотим отобразить статью:

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const includeURL = new URL(url);
  includeURL.pathname += 'include';

  if (isArticleURL(url)) {
    event.respondWith(async function() {
      // We're going to build a single request from multiple parts.
      const parts = [
        // The top of the page.
        caches.match('/article-top.include'),
        // The primary content
        fetch(includeURL)
          // A fallback if the network fails.
          .catch(() => caches.match('/article-offline.include')),
        // The bottom of the page
        caches.match('/article-bottom.include')
      ];

      // Merge them all together.
      const {done, response} = await mergeResponses(parts);

      // Wait until the stream is complete.
      event.waitUntil(done);

      // Return the merged response.
      return response;
    }());
  }
});

В приведенном выше примере mergeResponses — это небольшая функция , которая объединяет потоки каждого запроса. Это означает, что мы можем отображать кэшированный заголовок во время потоковой передачи сетевого контента.

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

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

Для поддержки этого заголовок отправляется с каждым запросом предварительной загрузки:

Service-Worker-Navigation-Preload: true

Сервер может использовать это для отправки другого контента для запросов предварительной загрузки навигации, чем для обычного запроса навигации. Просто не забудьте добавить заголовок Vary: Service-Worker-Navigation-Preload , чтобы кеши знали, что ваши ответы различаются.

Теперь мы можем использовать запрос предварительной загрузки:

// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
  // Else do a normal fetch
  .then(r => r || fetch(includeURL))
  // A fallback if the network fails.
  .catch(() => caches.match('/article-offline.include'));

const parts = [
  caches.match('/article-top.include'),
  networkContent,
  caches.match('/article-bottom')
];

Изменить заголовок

По умолчанию значение заголовка Service-Worker-Navigation-Preload равно true , но вы можете установить для него любое значение:

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
  console.log('Done!');
});

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

Получение состояния

Вы можете просмотреть состояние предварительной загрузки навигации, используя getState :

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.getState();
}).then(state => {
  console.log(state.enabled); // boolean
  console.log(state.headerValue); // string
});

Большое спасибо Мэтту Фалькенхагену и Цуёси Хоро за работу над этой функцией и помощь в написании этой статьи. И огромное спасибо всем, кто участвует в усилиях по стандартизации.