Przyspieszenie skryptu service worker dzięki wstępnym ładowaniu nawigacji

Wstępne wczytywanie nawigacji pozwala skrócić czas uruchamiania usługi Service Worker dzięki równoległemu wysyłaniu żądań.

Jake Archibald
Jake Archibald

Browser Support

  • Chrome: 59.
  • Edge: 18.
  • Firefox: 99.
  • Safari: 15.4.

Source

Podsumowanie

Problem

Gdy przejdziesz do witryny, która używa service workera do obsługi zdarzeń pobierania, przeglądarka poprosi go o odpowiedź. Obejmuje to uruchomienie procesu service worker (jeśli nie jest jeszcze uruchomiony) i wysłanie zdarzenia pobierania.

Czas uruchamiania zależy od urządzenia i warunków. Zwykle wynosi około 50 ms. Na urządzeniach mobilnych jest to około 250 ms. W skrajnych przypadkach (wolne urządzenia, obciążony procesor) może to być ponad 500 ms. Jednak ponieważ proces roboczy usługi pozostaje aktywny przez określony przez przeglądarkę czas między zdarzeniami, to opóźnienie występuje tylko sporadycznie, np. gdy użytkownik przechodzi do Twojej witryny z nowej karty lub innej witryny.

Czas uruchamiania nie stanowi problemu, jeśli odpowiadasz z pamięci podręcznej, ponieważ korzyść z pominięcia sieci jest większa niż opóźnienie uruchamiania. Jeśli jednak odpowiadasz za pomocą sieci…

Uruchamianie oprogramowania
Prośba o nawigację

Żądanie sieciowe jest opóźnione przez uruchomienie usługi Service Worker.

Stale skracamy czas uruchamiania, korzystając z pamięci podręcznej kodu w V8, pomijając procesy service worker, które nie mają zdarzenia pobierania, spekulatywnie uruchamiając procesy service worker i stosując inne optymalizacje. Czas uruchamiania zawsze będzie jednak większy niż zero.

Facebook zwrócił naszą uwagę na wpływ tego problemu i poprosił o możliwość wykonywania żądań nawigacji równolegle:

Uruchamianie oprogramowania
Prośba o nawigację

Wstępne wczytywanie nawigacji

Wstępne wczytywanie nawigacji to funkcja, która pozwala określić: „Gdy użytkownik wyśle żądanie nawigacji GET, rozpocznij żądanie sieciowe podczas uruchamiania usługi Service Worker”.

Opóźnienie przy uruchamianiu nadal występuje, ale nie blokuje żądania sieciowego, więc użytkownik szybciej otrzymuje treści.

Oto film pokazujący działanie tej funkcji. W tym przypadku opóźnienie uruchomienia service workera wynosi 500 ms, co uzyskano za pomocą pętli while:

Oto prezentacja Aby korzystać z zalet wstępnego wczytywania nawigacji, musisz mieć przeglądarkę, która je obsługuje.

Aktywowanie wstępnego ładowania nawigacji

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

Możesz w każdej chwili zadzwonić pod numer navigationPreload.enable() lub wyłączyć tę funkcję za pomocą przycisku navigationPreload.disable(). Ponieważ jednak Twoje zdarzenie fetch musi z niej korzystać, najlepiej jest włączać i wyłączać ją w zdarzeniu activate w usłudze Service Worker.

Korzystanie z wstępnie wczytanej odpowiedzi

Przeglądarka będzie teraz wstępnie wczytywać dane do nawigacji, ale nadal musisz używać odpowiedzi:

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 to obietnica, która jest realizowana w postaci odpowiedzi, jeśli:

  • Wstępne wczytywanie nawigacji jest włączone.
  • Żądanie jest żądaniem GET.
  • Żądanie jest żądaniem nawigacyjnym (generowanym przez przeglądarki podczas wczytywania stron, w tym elementów iframe).

W przeciwnym razie event.preloadResponse nadal będzie istnieć, ale będzie rozwiązywać się za pomocą undefined.

Jeśli strona potrzebuje danych z sieci, najszybszym sposobem jest wysłanie żądania w usłudze Service Worker i utworzenie pojedynczej przesyłanej strumieniowo odpowiedzi zawierającej części z pamięci podręcznej i części z sieci.

Załóżmy, że chcemy wyświetlić artykuł:

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

W powyższym przykładzie mergeResponses to mała funkcja, która łączy strumienie każdego żądania. Oznacza to, że możemy wyświetlać nagłówek z pamięci podręcznej, podczas gdy treści z sieci są przesyłane strumieniowo.

Jest to szybsze niż model „szkieletu aplikacji”, ponieważ żądanie sieciowe jest wysyłane wraz z żądaniem strony, a treści mogą być przesyłane strumieniowo bez większych obejść.

Żądanie includeURL zostanie jednak opóźnione o czas uruchomienia skryptu service worker. Do rozwiązania tego problemu możemy też użyć wstępnego wczytywania nawigacji, ale w tym przypadku nie chcemy wstępnie wczytywać całej strony, tylko plik dołączany.

Aby to umożliwić, z każdym żądaniem wstępnego wczytywania wysyłany jest nagłówek:

Service-Worker-Navigation-Preload: true

Serwer może używać tego sygnału do wysyłania różnych treści w przypadku żądań wstępnego wczytywania nawigacji niż w przypadku zwykłych żądań nawigacji. Pamiętaj tylko, aby dodać nagłówek Vary: Service-Worker-Navigation-Preload, aby pamięci podręczne wiedziały, że Twoje odpowiedzi się różnią.

Teraz możemy użyć żądania wstępnego wczytywania:

// 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')
];

Zmienianie nagłówka

Domyślnie wartość nagłówka Service-Worker-Navigation-Preload to true, ale możesz ustawić dowolną wartość:

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

Możesz na przykład ustawić go na identyfikator ostatniego wpisu zapisanego lokalnie w pamięci podręcznej, aby serwer zwracał tylko nowsze dane.

Pobieranie stanu

Stan wstępnego wczytywania nawigacji możesz sprawdzić za pomocą tego kodu: getState

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

Dziękujemy Mattowi Falkenhagenowi i Tsuyoshi Horo za pracę nad tą funkcją i pomoc w przygotowaniu tego artykułu. Ogromne podziękowania dla wszystkich osób zaangażowanych w proces standaryzacji.