Tăng tốc dịch vụ với nội dung tải trước điều hướng

Tính năng tải trước điều hướng cho phép bạn vượt qua thời gian khởi động trình chạy dịch vụ bằng cách đưa ra các yêu cầu song song.

Jake Archibald
Jake Archibald

Hỗ trợ trình duyệt

  • Chrome: 59.
  • Cạnh: 18.
  • Firefox: 99.
  • Safari: 15.4.

Nguồn

Tóm tắt

Vấn đề

Khi bạn điều hướng đến một trang web sử dụng trình chạy dịch vụ để xử lý các sự kiện tìm nạp, trình duyệt sẽ yêu cầu trình chạy dịch vụ đó phản hồi. Việc này liên quan đến việc khởi động trình chạy dịch vụ (nếu trình chạy dịch vụ chưa chạy) và điều phối sự kiện tìm nạp.

Thời gian khởi động phụ thuộc vào thiết bị và điều kiện. Quá trình này thường mất khoảng 50 mili giây. Trên thiết bị di động, nó tương tự như 250 mili giây. Trong các trường hợp nghiêm trọng (thiết bị chậm, CPU gặp vấn đề), thời gian này có thể vượt quá 500 mili giây. Tuy nhiên, vì trình chạy dịch vụ luôn hoạt động trong một khoảng thời gian do trình duyệt xác định giữa các sự kiện, nên thỉnh thoảng bạn chỉ gặp phải sự chậm trễ này, chẳng hạn như khi người dùng điều hướng đến trang web của bạn từ một thẻ mới hoặc một trang web khác.

Thời gian khởi động không phải là vấn đề nếu bạn phản hồi từ bộ nhớ đệm, vì lợi ích của việc bỏ qua mạng lớn hơn độ trễ khởi động. Nhưng nếu bạn đang phản hồi bằng mạng...

Khởi động SW
Yêu cầu đi theo chỉ dẫn

Yêu cầu mạng bị trì hoãn do trình khởi động dịch vụ khởi động.

Chúng tôi đang tiếp tục giảm thời gian khởi động bằng cách sử dụng tính năng lưu mã vào bộ nhớ đệm trong V8, cụ thể là bỏ qua các trình chạy dịch vụ không có sự kiện tìm nạp, bằng cách khởi động trình chạy dịch vụ theo suy đoán, cùng các biện pháp tối ưu hoá khác. Tuy nhiên, thời gian khởi động sẽ luôn lớn hơn 0.

Facebook đã thông báo cho chúng tôi về tác động của vấn đề này và yêu cầu cách thực hiện song song các yêu cầu điều hướng:

Khởi động SW
Yêu cầu đi theo chỉ dẫn

Tải trước tính năng điều hướng để khôi phục

Tải trước điều hướng là một tính năng cho phép bạn nói: "Khi người dùng đưa ra yêu cầu điều hướng GET, hãy bắt đầu yêu cầu mạng trong khi khởi động service worker".

Độ trễ khởi động vẫn còn nhưng không chặn yêu cầu mạng để người dùng nhận được nội dung sớm hơn.

Dưới đây là video minh hoạ hoạt động thực tế, trong đó trình chạy dịch vụ được đưa ra độ trễ khởi động có chủ ý 500 mili giây nhờ sử dụng vòng lặp while:

Sau đây là bản minh hoạ. Để tận dụng tính năng tải trước điều hướng, bạn cần có một trình duyệt hỗ trợ tính năng này.

Kích hoạt tính năng tải trước tính năng điều hướng

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

Bạn có thể gọi navigationPreload.enable() bất cứ khi nào bạn muốn hoặc tắt tính năng này bằng navigationPreload.disable(). Tuy nhiên, vì sự kiện fetch của bạn cần sử dụng hàm này, tốt nhất bạn nên bật và tắt sự kiện này trong sự kiện activate của trình chạy dịch vụ.

Sử dụng câu trả lời được tải trước

Bây giờ, trình duyệt sẽ thực hiện tải trước các thao tác điều hướng, nhưng bạn vẫn cần sử dụng phản hồi này:

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 là một lời hứa sẽ phân giải bằng một phản hồi, nếu:

  • Tính năng tải trước tính năng điều hướng đang bật.
  • Yêu cầu đó là yêu cầu GET.
  • Yêu cầu là yêu cầu điều hướng (mà các trình duyệt tạo khi đang tải trang, kể cả iframe).

Nếu không, event.preloadResponse vẫn ở đó nhưng được phân giải bằng undefined.

Nếu trang của bạn cần dữ liệu từ mạng, cách nhanh nhất là yêu cầu dữ liệu đó trong Service worker và tạo một phản hồi được truyền trực tuyến chứa các phần từ bộ nhớ đệm và các phần từ mạng.

Giả sử chúng tôi muốn hiển thị một bài viết:

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;
    }());
  }
});

Ở trên, mergeResponses là một hàm nhỏ hợp nhất các luồng của từng yêu cầu. Điều này có nghĩa là chúng tôi có thể hiển thị tiêu đề được lưu vào bộ nhớ đệm trong khi nội dung mạng truyền trực tuyến.

Thao tác này nhanh hơn "shell ứng dụng" khi yêu cầu mạng được đưa ra cùng với yêu cầu trang và nội dung có thể phát trực tuyến mà không bị các vụ tấn công nghiêm trọng.

Tuy nhiên, yêu cầu cho includeURL sẽ bị trì hoãn theo thời gian khởi động của trình chạy dịch vụ. Chúng ta cũng có thể sử dụng tính năng tải trước điều hướng để khắc phục vấn đề này. Tuy nhiên, trong trường hợp này, chúng ta không muốn tải trước toàn bộ trang nên chúng ta muốn tải trước một tệp include.

Để hỗ trợ việc này, một tiêu đề sẽ được gửi cùng với mỗi yêu cầu tải trước:

Service-Worker-Navigation-Preload: true

Máy chủ có thể sử dụng mã này để gửi nội dung khác cho các yêu cầu tải trước thông tin điều hướng so với cách gửi cho một yêu cầu điều hướng thông thường. Bạn chỉ cần nhớ thêm tiêu đề Vary: Service-Worker-Navigation-Preload để bộ nhớ đệm biết rằng các phản hồi của bạn sẽ khác nhau.

Bây giờ, chúng ta có thể sử dụng yêu cầu tải trước:

// 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')
];

Thay đổi tiêu đề

Theo mặc định, giá trị của tiêu đề Service-Worker-Navigation-Preloadtrue, nhưng bạn có thể thiết lập giá trị này thành bất kỳ giá trị nào bạn muốn:

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

Ví dụ: bạn có thể đặt mã thành ID của bài đăng mới nhất mà bạn đã lưu vào bộ nhớ đệm cục bộ, để máy chủ chỉ trả về dữ liệu mới hơn.

Lấy thông tin về tiểu bang

Bạn có thể tra cứu trạng thái tải trước điều hướng bằng cách sử dụng getState:

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

Rất cảm ơn Matt Falkenhagen và Tsuyoshi Horo vì đã nghiên cứu tính năng này và trợ giúp trong bài viết này. Xin chân thành cảm ơn những người tham gia vào nỗ lực chuẩn hoá