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

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

Jake Archibald
Jake Archibald

브라우저 지원

  • Chrome: 59. <ph type="x-smartling-placeholder">
  • Edge: 18. <ph type="x-smartling-placeholder">
  • Firefox: 99. <ph type="x-smartling-placeholder">
  • Safari 15.4. <ph type="x-smartling-placeholder">

소스

요약

문제

가져오기 이벤트를 처리하기 위해 서비스 워커를 사용하는 사이트로 이동하면 브라우저가 서비스 워커에 응답을 요청합니다. 그러려면 서비스 워커가 아직 실행되고 있지 않은 경우 이를 부팅하고 가져오기 이벤트를 전달해야 합니다.

부팅 시간은 기기 및 조건에 따라 다릅니다. 일반적으로 약 50ms입니다 모바일에서는 250ms에 가깝습니다. 극단적인 경우 (느린 기기, CPU 고장) 500ms 이상일 수 있습니다. 그러나 서비스 워커는 이벤트 사이에 브라우저에서 결정한 시간 동안 절전모드를 해제 상태로 유지하므로, 사용자가 새로운 탭이나 다른 사이트에서 사이트로 이동할 때와 같이 이 지연이 발생하는 경우는 가끔 있습니다.

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

SW 부팅
탐색 요청

서비스 워커가 부팅되는 동안 네트워크 요청이 지연됩니다.

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

Facebook은 이 문제의 영향에 대해 주의를 기울이고 동시에 내비게이션 요청을 수행하는 방법을 요청했습니다.

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 요청은 서비스 워커의 시작 시간만큼 지연됩니다. 탐색 미리 로드를 사용하여 이 문제도 수정할 수 있지만, 이 경우 전체 페이지를 미리 로드하지 않고 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!');
});

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

상태 가져오기

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

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

이 기능을 위해 노력해 주시고 기사에 도움을 주신 Matt Falkenhagen과 Tsuyoshi Horo에게 진심으로 감사드립니다. 아울러 표준화 노력에 참여해 주신 모든 분들께 진심으로 감사드립니다.