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 thành phần điều hướng cho phép bạn vượt qua thời gian khởi động của trình chạy dịch vụ bằng cách đưa ra các yêu cầu song song.

Jake Archibald
J Jake Archibald

Hỗ trợ trình duyệt

  • 59
  • 18
  • 99
  • 15,4

Nguồn

Tóm tắt

Vấn đề

Khi bạn chuyển đế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ụ này cung cấp phản hồi. Quá trình này bao gồm việc khởi động trình chạy dịch vụ (nếu trình chạy này chưa chạy) và gử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. Thường là khoảng 50 mili giây. Trên thiết bị di động, khoảng thời gian này thường là 250 mili giây. Trong các trường hợp nghiêm trọng (thiết bị hoạt động chậm, CPU gặp sự cố), thời lượng có thể vượt quá 500 mili giây. Tuy nhiên, do nhân viên dịch vụ không 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 sẽ gặp phải hiện tượng trễ, 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. Tuy nhiên, nếu bạn đang phản hồi bằng mạng...

Khởi động hướng Nam
Yêu cầu điều hướng

Yêu cầu mạng bị trì hoãn do trình chạy 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 phiên bản 8, thông qua 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 chạy trình chạy dịch vụ theo suy đoán và các phương 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 chúng tôi đưa ra cách thức thực hiện song song các yêu cầu chỉ đường:

Khởi động hướng Nam
Yêu cầu điều hướng



Chúng tôi phản hồi: "có, có vẻ công bằng".

Tính năng "Tải trước tính năng chỉ đường" để khôi phục

Tải trước thành phần điều hướng là một tính năng cho phép bạn nói: "Xin chào, 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 trình chạy dịch vụ đang khởi động".

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

Dưới đây là video về hoạt động thực tế của worker, trong đó worker được trì hoãn khởi động có chủ ý 500 mili giây bằng vòng lặp while:

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

Đang kích hoạt tải trước thành phần đ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ần sử dụng lớp này, tốt nhất bạn nên bật/tắt sự kiện này trong sự kiện activate của trình chạy dịch vụ.

Sử dụng phản hồi được tải trước

Bây giờ, trình duyệt sẽ tải trước các thao tác di chuyển, 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ẽ giải quyết bằng một phản hồi, nếu:

  • Tính năng tải trước thành phần điều hướng đang bật.
  • Yêu cầu là một yêu cầu GET.
  • Yêu cầu này là một yêu cầu điều hướng (mà trình duyệt tạo ra khi chúng tải trang, bao gồm cả iframe).

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

Nếu trang của bạn cần dữ liệu từ mạng, thì cách nhanh nhất là yêu cầu dữ liệu đó trong trình chạy dịch vụ 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 ta 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 mỗi 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 trong bộ nhớ đệm trong khi nội dung mạng truyền trực tuyến.

Mô hình này nhanh hơn mô hình "app shell" vì yêu cầu mạng được thực hiện cùng với yêu cầu trang và nội dung có thể phát trực tuyến mà không cần đến các vụ tấn công lớn.

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 thành phần điều hướng để khắc phục vấn đề này, nhưng trong trường hợp này, chúng ta không muốn tải trước toàn bộ trang mà nên tải trước một thành phần.

Để hỗ trợ việc này, một tiêu đề sẽ được gửi đ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 lớp này để gửi nội dung khác cho các yêu cầu tải trước điều hướng so với 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 không giống nhau.

Giờ đây, 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ể đặt 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 giá trị này 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 trạng thái

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

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

Một phần của Loạt chương trình tương tác mới