탐색 미리 로드로 서비스 워커 속도 향상

탐색 미리 로드를 사용하면 요청을 동시에 실행하여 서비스 워커 시작 시간을 줄일 수 있습니다.

Jake Archibald
Jake Archibald

브라우저 지원

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

소스

요약

문제

서비스 워커를 사용하여 가져오기 이벤트를 처리하는 사이트로 이동하면 브라우저가 서비스 워커에 응답을 요청합니다. 여기에는 서비스 워커를 부팅 (아직 실행 중이 아닌 경우)하고 가져오기 이벤트를 전달하는 작업이 포함됩니다.

부팅 시간은 기기와 조건에 따라 다릅니다. 일반적으로 50ms 정도입니다. 모바일에서는 250ms 정도입니다. 극단적인 경우 (느린 기기, CPU 과부하) 500ms를 초과할 수 있습니다. 그러나 서비스 워커는 이벤트 간에 브라우저에서 결정한 시간 동안 계속 작동하므로 사용자가 새 탭이나 다른 사이트에서 사이트로 이동할 때와 같이 가끔 이러한 지연이 발생합니다.

캐시에서 응답하는 경우 부팅 시간이 문제가 되지 않습니다. 네트워크를 건너뛰는 이점이 부팅 지연보다 크기 때문입니다. 하지만 네트워크를 사용하여 응답하는 경우…

SW 부팅
내비게이션 요청

서비스 워커 부팅으로 인해 네트워크 요청이 지연됩니다.

V8에서 코드 캐싱을 사용하고, 가져오기 이벤트가 없는 서비스 워커를 건너뛰고, 서비스 워커를 추측적으로 실행하는 등의 최적화를 통해 부팅 시간을 계속 줄이고 있습니다. 하지만 부팅 시간은 항상 0보다 큽니다.

Facebook에서 이 문제의 영향을 Google에 알리고 탐색 요청을 동시에 실행하는 방법을 요청했습니다.

SW 부팅
내비게이션 요청

탐색 미리 로드가 구출

탐색 미리 로드는 '사용자가 GET 탐색 요청을 하면 서비스 워커가 부팅되는 동안 네트워크 요청을 시작합니다'라고 말할 수 있는 기능입니다.

시작 지연은 여전히 있지만 네트워크 요청을 차단하지 않으므로 사용자가 더 빨리 콘텐츠를 가져올 수 있습니다.

다음은 while 루프를 사용하여 서비스 워커의 시작 지연을 500ms로 고의적으로 지연하는 동영상입니다.

다음은 데모 자체입니다. 탐색 미리 로드의 이점을 누리려면 이를 지원하는 브라우저가 필요합니다.

탐색 미리 로드 활성화

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

예를 들어 로컬에 캐시한 마지막 게시물의 ID로 설정하면 서버에서 최신 데이터만 반환합니다.

상태 가져오기

getState를 사용하여 탐색 미리 로드 상태를 조회할 수 있습니다.

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

이 기능을 개발하고 이 도움말을 작성하는 데 도움을 주신 매트 팔켄하겐님과 호로 츠요시님께 감사드립니다. 표준화 작업에 참여해 주신 모든 분께 감사드립니다.