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

Wstępne wczytanie nawigacji pozwala skrócić czas uruchamiania instancji roboczej usługi przez równoległe wysyłanie żądań.

Jake Archibald
Jake Archibald

Obsługa przeglądarek

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

Źródło

Podsumowanie

Problem

Gdy przechodzisz do witryny, która do obsługi zdarzeń pobierania używa skryptu service worker, przeglądarka prosi o odpowiedź. Wymaga to uruchomienia skryptu service worker (jeśli jeszcze nie jest uruchomiony) i wysyłania zdarzenia pobierania.

Czas uruchamiania zależy od urządzenia i warunków. Zwykle zajmuje to około 50 ms. Na komórkach trwa to już ponad 250 ms. W skrajnych przypadkach (powolne urządzenia lub problemy z procesorem) wartość może przekraczać 500 ms. Ponieważ jednak mechanizm Service Worker pozostaje aktywny między zdarzeniami przez czas określony przez przeglądarkę, opóźnienie występuje tylko od czasu do czasu, np. gdy użytkownik przechodzi do Twojej witryny z nowej karty lub z innej witryny.

Czas uruchamiania nie stanowi problemu, jeśli odpowiadasz z pamięci podręcznej, ponieważ pominięcie sieci jest większe niż opóźnienie uruchamiania. Jeśli jednak odpowiadasz, korzystając z sieci...

Uruchamianie systemu
Prośba o nawigację

Żądanie sieciowe jest opóźnione przez uruchamianie skryptu service worker.

Nadal skracamy czas uruchamiania, używając buforowania kodu w wersji 8, pomijając mechanizmy Service Worker, które nie mają zdarzenia pobierania, uruchamiając mechanizmy Service Worker w sposób spekulacyjny i stosując inne optymalizacje. Jednak czas uruchamiania będzie zawsze większy od zera.

Facebook zwrócił naszą uwagę na wpływ tego problemu i poprosił o opracowanie sposobu równoległego wykonywania żądań związanych z nawigacją:

Uruchamianie systemu
Prośba o nawigację

Nawigacja wstępnie wczytuje się na ratunek

Wstępne wczytywanie nawigacji to funkcja, która pozwala powiedzieć: „Gdy użytkownik wysyła żądanie nawigacji GET, uruchom żądanie sieciowe podczas uruchamiania skryptu service worker”.

Opóźnienie uruchomienia nadal występuje, ale nie blokuje żądania sieciowego, więc użytkownik zapoznaje się z treścią wcześniej.

Oto film pokazujący, jak to działa. Skrypt service worker ma celowe 500 ms opóźnienia uruchomienia z wykorzystaniem pętli „if”:

Oto jej wersja demonstracyjna. Aby skorzystać z zalet wstępnego wczytywania nawigacji, musisz mieć przeglądarkę, która ją obsługuje.

Aktywuj wstępne wczytywanie nawigacji

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

Możesz zadzwonić do navigationPreload.enable() w dowolnym momencie lub wyłączyć go za pomocą navigationPreload.disable(). Zdarzenie fetch musi jednak z niego korzystać, więc najlepiej jest je włączać i wyłączać w zdarzeniu activate skryptu service worker.

Używanie wstępnie wczytanej odpowiedzi

Teraz przeglądarka rozpocznie wstępne wczytywanie elementów nawigacyjnych, ale nadal musisz skorzystać z 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 kończy się odpowiedzią, jeśli:

  • Włączone jest wstępne wczytywanie nawigacji.
  • Żądanie ma postać GET.
  • Żądanie to żądanie nawigacji (które przeglądarki generują podczas wczytywania stron, łącznie z elementami iframe).

W przeciwnym razie błąd event.preloadResponse jest nadal dostępny, ale występuje błąd undefined.

Jeśli strona potrzebuje danych z sieci, najszybszym sposobem jest żądanie ich w skrypcie service worker i utworzenie pojedynczej odpowiedzi strumieniowej 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 przesyłania treści z sieci.

To szybsze niż „powłoka aplikacji” gdy żądanie sieciowe jest wysyłane razem z żądaniem strony, dzięki czemu treści mogą być przesyłane strumieniowo bez poważnych ataków hakerów.

Żądanie dotyczące includeURL będzie jednak opóźnione o czas uruchamiania skryptu service worker. Aby rozwiązać ten problem, możemy też użyć wstępnego wczytywania nawigacji, ale w tym przypadku nie chcemy wstępnie wczytywać całej strony, tylko wstępnie ładować stronę z uwzględnieniem.

W tym celu razem z każdym żądaniem wstępnego wczytywania wysyłany jest nagłówek:

Service-Worker-Navigation-Preload: true

Serwer może go używać do wysyłania innych treści w żądaniach wstępnego wczytywania nawigacji niż w przypadku zwykłych żądań nawigacji. Pamiętaj tylko o dodaniu nagłówka Vary: Service-Worker-Navigation-Preload, aby pamięci podręczne wiedzieli, ż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')
];

Zmiana nagłówka

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

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

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

Pobieram informacje o stanie

Stan wstępnego wczytywania nawigacji możesz sprawdzić za pomocą narzędzia 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 Tsuyoshim Horo za pracę nad tą funkcją oraz za pomoc przy tym artykule. Serdecznie dziękujemy wszystkim osobom zaangażowanym w pracę na rzecz standaryzacji.