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

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

Джейк Арчибальд
Jake Archibald

Browser Support

  • Хром: 59.
  • Край: 18.
  • Firefox: 99.
  • Сафари: 15.4.

Source

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

Проблема

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

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

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

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

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

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

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

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

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

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

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

Вот видео, демонстрирующее работу этого процесса, где сервисному работнику задается намеренная задержка запуска в 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 запрос.
  • Запрос представляет собой навигационный запрос (который браузеры генерируют при загрузке страниц, включая фреймы).

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

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

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

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

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

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
});

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