עומס מראש של ניווט מאפשר לך להתגבר על זמן ההפעלה של Service Worker על ידי שליחת בקשות במקביל.
סיכום
- במצבים מסוימים, זמן האתחול של Service Worker עלול לעכב את תגובת הרשת.
- התכונה טעינה מראש של ניווט, זמינה בשלושת מנועי הדפדפנים העיקריים, מאפשרת לבצע את הבקשה במקביל לאתחול של Service Worker.
- ניתן להבחין בין בקשות טעינה מראש לבין ניווטים רגילים באמצעות כותרת ולהציג תוכן שונה.
הבעיה
כשתנווטו לאתר שמשתמש ב-Service Worker כדי לטפל באירועי אחזור, הדפדפן יבקש תגובה מ-Service Worker. פעולות אלה כוללות אתחול של ה-Service Worker (אם הוא עדיין לא פועל) ושליחת אירוע האחזור.
זמן האתחול תלוי במכשיר ובתנאים. הבדיקה נמשכת בדרך כלל כ-50 אלפיות השנייה. בנייד, משך הזמן הוא יותר מ-250 אלפיות השנייה. במקרים קיצוניים (מכשירים איטיים, מעבד (CPU) במצוקה) הטעינה עשויה להימשך יותר מ-500 אלפיות השנייה. עם זאת, מאחר שה-Service Worker נשאר ער למשך זמן מסוים בין אירועים שנקבע על ידי הדפדפן, העיכוב הזה יופיע רק מדי פעם, למשל כשהמשתמש גולש לאתר שלכם מכרטיסייה חדשה או מאתר אחר.
זמן האתחול אינו בעיה אם אתם מגיבים מהמטמון, כי היתרון של דילוג על הרשת גדול יותר מעיכוב האתחול. אבל אם אתם מגיבים באמצעות הרשת...
בקשת הרשת מתעכבת בגלל אתחול של Service Worker.
אנחנו ממשיכים לקצר את זמן האתחול על ידי שימוש בשמירת קוד במטמון ב-V8, על ידי דילוג על קובצי שירות (service worker) שאין להם אירוע אחזור, על ידי השקה ספקולטיבית של קובצי שירות (service worker) ופעולות אופטימיזציה אחרות. עם זאת, זמן האתחול תמיד יהיה גדול מאפס.
Facebook הסבה את תשומת לבנו להשפעה של הבעיה הזו, וביקשה דרך לבצע בקשות ניווט במקביל:
טעינה מראש של הניווט להצלה
טעינה מראש של ניווט היא תכונה שמאפשרת להגיד "כשהמשתמש שולח בקשת ניווט מסוג GET, צריך להפעיל את בקשת הרשת בזמן שה-Service Worker מופעל".
עיכוב ההפעלה עדיין קיים, אבל הוא לא חוסם את בקשת הרשת, כך שהמשתמש מקבל את התוכן מוקדם יותר.
בסרטון הבא מוצג סרטון פעולה, שבו ל-Service Worker יש עיכוב מכוון בהפעלה של 500 אלפיות השנייה באמצעות לולאת זמן:
זו ההדגמה עצמה. כדי ליהנות מהיתרונות של טעינה מראש של ניווט, צריך דפדפן שתומך באפשרות הזו.
הפעלת טעינה מראש של ניווט
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
. - הבקשה היא בקשת ניווט (אילו דפדפנים יוצרים כשהם טוענים דפים, כולל iframes).
אחרת, 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
היא פונקציה קטנה שממזגת את מקורות הנתונים של כל בקשה. כלומר, אנחנו יכולים להציג את הכותרת שנשמרה במטמון בזמן הסטרימינג של התוכן ברשת.
המודל הזה מהיר יותר מהמודל 'מעטפת אפליקציה', כי בקשת הרשת נשלחת יחד עם בקשת הדף, והתוכן יכול להיות זמין בסטרימינג ללא פריצות ראשיות.
עם זאת, הבקשה אל includeURL
תתעכב בזמן ההפעלה של ה-Service Worker. כדי לתקן גם את הבעיה, נוכל להשתמש בטעינה מראש של ניווט, אך במקרה זה איננו רוצים לטעון מראש את הדף המלא, אלא אנו רוצים לטעון מראש הכללה.
כדי לתמוך בכך, נשלחת כותרת עם כל בקשה לטעינה מראש:
Service-Worker-Navigation-Preload: true
השרת יכול להשתמש באפשרות הזו כדי לשלוח תוכן שונה לבקשות ניווט רגילות מאשר בבקשת ניווט רגילה. רק חשוב לזכור להוסיף את הכותרת Vary: Service-Worker-Navigation-Preload
, כדי שהתשובות שלך יישמרו במטמון של Google.
עכשיו נוכל להשתמש בבקשת הטעינה מראש:
// 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
});
תודה רבה למאט פלקנהאגן ולצויושי הורו על עבודתם לתכונה זו, ועזרה במאמר זה. ותודה רבה לכל מי שמעורב במאמץ לתקינה