Tính năng tải trước điều hướng cho phép bạn khắc phục thời gian khởi động trình chạy dịch vụ bằng cách thực hiện các yêu cầu song song.
Tóm tắt
- Trong một số trường hợp, thời gian khởi động trình chạy dịch vụ có thể làm chậm phản hồi mạng.
- Có sẵn trong 3 công cụ trình duyệt chính, tính năng tải trước điều hướng sẽ khắc phục vấn đề này bằng cách cho phép bạn thực hiện yêu cầu song song với quá trình khởi động trình chạy dịch vụ.
- Bạn có thể phân biệt các yêu cầu tải trước với các thao tác điều hướng thông thường bằng cách sử dụng tiêu đề và phân phát nội dung khác nhau.
Vấn đề
Khi bạn điều hướng đến một trang web sử dụng worker dịch vụ để xử lý các sự kiện tìm nạp, trình duyệt sẽ yêu cầu worker dịch vụ phản hồi. Thao tác này bao gồm việc khởi động trình chạy dịch vụ (nếu 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. Thông thường, khoảng thời gian này là khoảng 50 mili giây. Trên thiết bị di động, khoảng thời gian này là 250 mili giây. Trong trường hợp cực đoan (thiết bị chậm, CPU gặp sự cố), thời gian này có thể lên đến hơn 500 mili giây. Tuy nhiên, vì worker dịch vụ vẫn thức 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 bạn chỉ thỉnh thoảng gặp phải độ trễ này, chẳng hạn như khi người dùng chuyển đế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 đang 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…
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 V8, bằng cách bỏ qua các worker dịch vụ không có sự kiện tìm nạp, bằng cách chạy worker dịch vụ một cách dự đoán và 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 một cách để thực hiện song song các yêu cầu điều hướng:
Tải trước điều hướng để giải cứu
Tải trước điều hướng là một tính năng cho phép bạn nói rằng "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ễ khởi động vẫn còn, 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ề cách hoạt động của tính năng này, trong đó worker dịch vụ được trì hoãn khởi động 500 mili giây một cách có chủ ý bằng cách sử dụng vòng lặp while:
Sau đây là bản minh hoạ. Để tận dụng các lợi ích của 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 đ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 bằng navigationPreload.disable()
. Tuy nhiên, vì sự kiện fetch
cần sử dụng tính năng này, nên tốt nhất bạn nên bật và tắt tính năng này trong sự kiện activate
của worker dịch vụ.
Sử dụng câu trả lời được tải trước
Giờ đây, trình duyệt sẽ thực hiện tải trước cho các thao tác điều hướng, nhưng bạn vẫn cần sử dụng phản hồi:
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 điều hướng đã được bật.
- Yêu cầu này là yêu cầu
GET
. - Yêu cầu này là một yêu cầu điều hướng (do trình duyệt tạo khi đang tải trang, bao gồm cả iframe).
Nếu không, event.preloadResponse
vẫn tồn tại nhưng sẽ phân giải bằng undefined
.
Câu trả lời tuỳ chỉnh cho tính năng tải trước
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 worker dịch vụ và tạo một phản hồi 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;
}());
}
});
Trong đoạn mã 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 ta có thể hiển thị tiêu đề đã lưu vào bộ nhớ đệm trong khi nội dung mạng truyền vào.
Cách này nhanh hơn mô hình "vỏ ứng dụng" 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ể truyền trực tuyến mà không cần các thao tác xâm nhập lớn.
Tuy nhiên, yêu cầu về includeURL
sẽ bị trì hoãn do 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, 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à muốn tải trước một phần tử bao gồm.
Để 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 thông tin 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. 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á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-Preload
là true
, 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 mã của bài đăng gần đây 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.
Nhận 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 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 đã nỗ lực phát triển tính năng này và giúp đỡ chúng tôi viết bài này. Và cảm ơn tất cả những người đã tham gia nỗ lực chuẩn hoá