האצת קובץ השירות (service worker) באמצעות טעינה מראש של ניווט

עומס מראש של ניווט מאפשר לך להתגבר על זמן ההפעלה של Service Worker על ידי שליחת בקשות במקביל.

ג'ייק ארצ'יבלד
ג'ייק ארצ'יבלד

תמיכה בדפדפן

  • 59
  • 18
  • 99
  • 15.4

מקור

סיכום

הבעיה

כשתנווטו לאתר שמשתמש ב-Service Worker כדי לטפל באירועי אחזור, הדפדפן יבקש תגובה מ-Service Worker. פעולות אלה כוללות אתחול של ה-Service Worker (אם הוא עדיין לא פועל) ושליחת אירוע האחזור.

זמן האתחול תלוי במכשיר ובתנאים. הבדיקה נמשכת בדרך כלל כ-50 אלפיות השנייה. בנייד, משך הזמן הוא יותר מ-250 אלפיות השנייה. במקרים קיצוניים (מכשירים איטיים, מעבד (CPU) במצוקה) הטעינה עשויה להימשך יותר מ-500 אלפיות השנייה. עם זאת, מאחר שה-Service Worker נשאר ער למשך זמן מסוים בין אירועים שנקבע על ידי הדפדפן, העיכוב הזה יופיע רק מדי פעם, למשל כשהמשתמש גולש לאתר שלכם מכרטיסייה חדשה או מאתר אחר.

זמן האתחול אינו בעיה אם אתם מגיבים מהמטמון, כי היתרון של דילוג על הרשת גדול יותר מעיכוב האתחול. אבל אם אתם מגיבים באמצעות הרשת...

אתחול SW
בקשה לניווט

בקשת הרשת מתעכבת בגלל אתחול של Service Worker.

אנחנו ממשיכים לקצר את זמן האתחול על ידי שימוש בשמירת קוד במטמון ב-V8, על ידי דילוג על קובצי שירות (service worker) שאין להם אירוע אחזור, על ידי השקה ספקולטיבית של קובצי שירות (service worker) ופעולות אופטימיזציה אחרות. עם זאת, זמן האתחול תמיד יהיה גדול מאפס.

Facebook הסבה את תשומת לבנו להשפעה של הבעיה הזו, וביקשה דרך לבצע בקשות ניווט במקביל:

אתחול SW
בקשה לניווט



ואנחנו אמרנו "כן, נראה הוגנים".

"טעינה מראש של הניווט" להצלה

טעינה מראש של ניווט היא תכונה שמאפשרת להגיד "Hey, כאשר המשתמש מבצע בקשת ניווט מסוג GET, start the network request when the 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
});

תודה רבה למאט פלקנהאגן ולצויושי הורו על עבודתם לתכונה זו, ועזרה במאמר זה. ותודה רבה לכל מי שמעורב במאמץ לתקינה

חלק מהסדרה החדשה של יכולת פעולה הדדית