การโหลดล่วงหน้าสำหรับการไปยังส่วนต่างๆ ช่วยให้คุณลดเวลาเริ่มต้นของ Service Worker ได้โดยการส่งคำขอพร้อมกัน
สรุป
- ในบางสถานการณ์ เวลาเริ่มต้นของ Service Worker อาจทำให้การตอบสนองของเครือข่ายล่าช้า
- การโหลดล่วงหน้าของการนำทางซึ่งพร้อมใช้งานในเครื่องมือเบราว์เซอร์หลัก 3 รายการจะแก้ไขปัญหานี้โดยให้คุณส่งคำขอควบคู่ไปกับการบูต Service Worker
- คุณแยกแยะคำขอโหลดล่วงหน้าจากการไปยังส่วนต่างๆ ปกติได้โดยใช้ส่วนหัว และแสดงเนื้อหาที่แตกต่างกัน
ปัญหา
เมื่อคุณไปยังเว็บไซต์ที่ใช้ Service Worker เพื่อจัดการเหตุการณ์การดึงข้อมูล เบราว์เซอร์จะขอคำตอบจาก Service Worker ซึ่งเกี่ยวข้องกับการบูต Service Worker (หากยังไม่ได้เรียกใช้) และการส่งเหตุการณ์ Fetch
เวลาในการบูตเครื่องจะขึ้นอยู่กับอุปกรณ์และเงื่อนไข โดยปกติจะอยู่ที่ประมาณ 50 มิลลิวินาที ส่วนในอุปกรณ์เคลื่อนที่จะอยู่ที่ประมาณ 250 มิลลิวินาที ในกรณีที่ร้ายแรง (อุปกรณ์ช้า, CPU ทำงานหนัก) อาจใช้เวลามากกว่า 500 มิลลิวินาที อย่างไรก็ตาม เนื่องจาก Service Worker จะทำงานตามระยะเวลาที่เบราว์เซอร์กำหนดระหว่างเหตุการณ์ คุณจึงอาจพบความล่าช้านี้เป็นครั้งคราว เช่น เมื่อผู้ใช้ไปยังเว็บไซต์จากแท็บใหม่หรือเว็บไซต์อื่น
เวลาเริ่มต้นระบบไม่ใช่ปัญหาหากคุณตอบจากแคช เนื่องจากประโยชน์ของการข้ามเครือข่ายมีมากกว่าความล่าช้าในการเริ่มต้นระบบ แต่หากคุณตอบกลับโดยใช้เครือข่าย...
Service Worker ที่บูตอัปทำให้คำขอจากเครือข่ายล่าช้า
เราจะลดเวลาในการบูตต่อไปโดยใช้การแคชโค้ดใน V8, ข้าม Service Worker ที่ไม่มีเหตุการณ์การดึงข้อมูล, เปิดใช้ Service Worker แบบคาดการณ์ และการเพิ่มประสิทธิภาพอื่นๆ อย่างไรก็ตาม เวลาในการบูตจะมากกว่า 0 เสมอ
Facebook ได้แจ้งให้เราทราบถึงผลกระทบของปัญหานี้ และขอให้เราหาวิธีส่งคำขอการนำทางแบบคู่ขนาน
การโหลดล่วงหน้าของการนำทางช่วยได้
การโหลดล่วงหน้าของการไปยังส่วนต่างๆ เป็นฟีเจอร์ที่ช่วยให้คุณพูดได้ว่า "เมื่อผู้ใช้ส่งคำขอไปยังส่วนต่างๆ แบบ GET ให้เริ่มคำขอเครือข่ายขณะที่ Service Worker กำลังบูต"
การหน่วงเวลาเริ่มต้นยังคงมีอยู่ แต่ไม่ได้บล็อกคำขอเครือข่าย ผู้ใช้จึงได้รับเนื้อหาเร็วขึ้น
นี่คือวิดีโอการทำงานของฟีเจอร์นี้ ซึ่งมีการหน่วงเวลาเริ่มต้นของ Service Worker เป็น 500 มิลลิวินาทีโดยใช้ลูป while
นี่คือเดโม หากต้องการรับประโยชน์จากการโหลดล่วงหน้าของการนำทาง คุณจะต้องใช้เบราว์เซอร์ที่รองรับ
เปิดใช้งานการโหลดล่วงหน้าของการนำทาง
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
เหตุการณ์ของ Service Worker
การใช้คำตอบที่โหลดไว้ล่วงหน้า
ตอนนี้เบราว์เซอร์จะทำการโหลดล่วงหน้าสำหรับการนำทาง แต่คุณยังคงต้องใช้การตอบสนอง
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
การตอบกลับที่กำหนดเองสำหรับการโหลดล่วงหน้า
หากหน้าเว็บต้องการข้อมูลจากเครือข่าย วิธีที่เร็วที่สุดคือการขอข้อมูลใน Service Worker และสร้างการตอบกลับแบบสตรีมรายการเดียวที่มีส่วนต่างๆ จากแคชและส่วนต่างๆ จากเครือข่าย
สมมติว่าเราต้องการแสดงบทความ
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
เป็นฟังก์ชันเล็กๆ ที่ผสานสตรีมของแต่ละคำขอ ซึ่งหมายความว่าเราสามารถแสดงส่วนหัวที่แคชไว้ในขณะที่เนื้อหาเครือข่ายสตรีมเข้ามา
ซึ่งเร็วกว่าโมเดล "App Shell" เนื่องจากคำขอเครือข่ายจะทำพร้อมกับคำขอหน้าเว็บ และเนื้อหาสามารถสตรีมได้โดยไม่ต้องใช้การแฮ็กที่สำคัญ
อย่างไรก็ตาม คำขอสำหรับ includeURL
จะล่าช้าเนื่องจากเวลาเริ่มต้นของ Service Worker เราใช้การโหลดล่วงหน้าของการนำทางเพื่อแก้ไขปัญหานี้ได้เช่นกัน แต่ในกรณีนี้เราไม่ต้องการโหลดหน้าเว็บแบบเต็มล่วงหน้า แต่ต้องการโหลดการรวมล่วงหน้า
เราจึงส่งส่วนหัวพร้อมกับคำขอโหลดล่วงหน้าทุกรายการเพื่อรองรับการดำเนินการนี้
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!');
});
เช่น คุณอาจตั้งค่าเป็นรหัสของโพสต์ล่าสุดที่แคชไว้ในเครื่อง เพื่อให้เซิร์ฟเวอร์แสดงเฉพาะข้อมูลใหม่กว่า
การรับสถานะ
คุณสามารถค้นหาสถานะของการโหลดล่วงหน้าของการนำทางได้โดยใช้ 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 ที่ทำงานเกี่ยวกับฟีเจอร์นี้และช่วยเขียนบทความนี้ และขอขอบคุณทุกคนที่เกี่ยวข้องกับความพยายามในการกำหนดมาตรฐาน