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

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

Jake Archibald
Jake Archibald

Obsługa przeglądarek

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

Źródło

Podsumowanie

Problem

Gdy przechodzisz do witryny, która używa usług workera do obsługi zdarzeń pobierania, przeglądarka prosi o odpowiedź. Polega to na uruchomieniu serwisowego workera (jeśli nie działa już) i wysłaniu zdarzenia fetch.

Czas uruchamiania zależy od urządzenia i warunków. Zwykle wynosi ona około 50 ms. Na urządzeniach mobilnych wynosi ona około 250 ms. W skrajnych przypadkach (wolne urządzenia, przeciążony procesor) może to być ponad 500 ms. Jednak ponieważ między zdarzeniami usługa robocza pozostaje aktywna przez czas określony przez przeglądarkę, opóźnienie występuje tylko sporadycznie, 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ż korzyści z pominięcia sieci są większe niż opóźnienie uruchamiania. Jeśli jednak odpowiadasz za pomocą sieci…

Uruchamianie SW
Prośba o nawigację

Żądanie sieciowe jest opóźnione przez uruchamianie usługi.

Cały czas skracamy czas uruchamiania, stosując użytkowanie pamięci podręcznej kodu w V8, pomijanie usług workerów, które nie mają zdarzenia pobierania, uruchamianie usług workerów w sposób spekulatywny oraz inne optymalizacje. Czas uruchamiania będzie jednak zawsze większy niż zero.

Facebook zwrócił naszą uwagę na wpływ tego problemu i poprosił o sposób równoległego wykonywania żądań nawigacyjnych:

Uruchamianie SW
Prośba o nawigację

Nawigacja z wstępnym wczytaniem na ratunek

Wczytywanie wstępne nawigacji to funkcja, która pozwala określić, że „gdy użytkownik wysyła żądanie GET nawigacji, należy rozpocząć żądanie sieci podczas uruchamiania pracownika usługi”.

Opóźnienie uruchamiania nadal występuje, ale nie blokuje żądania sieci, dzięki czemu użytkownik szybciej uzyskuje dostęp do treści.

Oto film przedstawiający działanie tego rozwiązania, w którym usługa workera jest celowo opóźniana o 500 ms za pomocą pętli while:

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

Aktywowanie wstępnego wczytania nawigacji

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

W każdej chwili możesz zadzwonić do navigationPreload.enable() lub wyłączyć tę funkcję za pomocą navigationPreload.disable(). Jednak ponieważ zdarzenie fetch musi z niego korzystać, najlepiej jest je włączyć i wyłączyć w zdarzeniu activate w usługach workera.

Korzystanie z wstępnie załadowanej odpowiedzi

Przeglądarka będzie teraz wykonywać pobieranie z wyprzedzeniem podczas nawigacji, ale nadal musisz użyć 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:

  • Wstępne wczytywanie nawigacji jest włączone.
  • Żądanie jest GET.
  • Żądanie to żądanie nawigacyjne (które przeglądarki generują podczas wczytywania stron, w tym iframe).

W przeciwnym razie event.preloadResponse nadal występuje, ale jest rozwiązywane przez undefined.

Jeśli Twoja strona potrzebuje danych z sieci, najszybszym sposobem jest wysłanie żądania w serwisie workera 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 poszczególnych żądań. Oznacza to, że możemy wyświetlić nagłówek z poziomu pamięci podręcznej, gdy treści z sieci są przesyłane strumieniowo.

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

Jednak żądanie includeURL zostanie opóźnione o czas uruchamiania skryptu service worker. Aby rozwiązać ten problem, możemy też użyć wstępnego pobierania elementów nawigacji, ale w tym przypadku nie chcemy wstępnie pobrać całej strony, tylko include.

Aby to umożliwić, nagłówek jest wysyłany z każdą prośbą o wstępny odczyt:

Service-Worker-Navigation-Preload: true

Serwer może wykorzystać tę funkcję, aby wysłać różne treści w ramach żądań wstępnego wczytania nawigacji niż w ramach zwykłego żądania nawigacji. Pamiętaj tylko, aby dodać nagłówek Vary: Service-Worker-Navigation-Preload, aby pamięć podręczna wiedziała, że Twoje odpowiedzi się różnią.

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

// 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ślna 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 posta, który masz w pamięci podręcznej na urządzeniu, aby serwer zwracał tylko nowsze dane.

Pobieranie stanu

Stan wstępnego wczytania nawigacji możesz sprawdzić za pomocą 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 Tsuyoshij Horo za pracę nad tą funkcją i pomoc w przygotowaniu tego artykułu. Dziękujemy wszystkim, którzy przyczynili się do standaryzacji.