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