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