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

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

제이크 아치볼드
제이크 아치볼드

브라우저 지원

  • 59
  • 18
  • 99
  • 15.4

소스

요약

문제

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

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

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

소프트웨어 부팅
내비게이션 요청

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

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

Facebook은 이 문제의 영향을 주목하고 동시에 탐색 요청을 수행할 수 있는 방법을 요청했습니다.

소프트웨어 부팅
내비게이션 요청



그러면 '네, 괜찮은 것 같네요'라고 했죠.

'탐색 미리 로드'를 사용하여 구조

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

시작 지연이 계속 유지되지만 네트워크 요청을 차단하지 않으므로 사용자가 콘텐츠를 더 빨리 사용할 수 있습니다.

다음은 이 동작을 보여주는 동영상이며, 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
});

이 기능과 관련해 도움을 주시고 이 기능과 관련해 도움을 주신 Matt Falkenhagen과 Tsuyoshi Horo에게 감사드립니다. 또한 표준화 작업에 참여해 주신 모든 분께 진심으로 감사드립니다.

새롭게 상호 운용 가능한 시리즈의 일부