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

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

Jake Archibald
Jake Archibald

תמיכה בדפדפן

  • Chrome: 59.
  • קצה: 18.
  • Firefox: 99.
  • Safari: 15.4.

מקור

סיכום

הבעיה

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

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

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

אתחול דר'-מע'
בקשת ניווט

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

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

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

אתחול דר'-מע'
בקשת ניווט

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

טעינה מראש של ניווט היא תכונה שמאפשרת לומר "כאשר המשתמש שולח בקשת ניווט GET, יש להפעיל את בקשת הרשת בזמן שה-Service Worker מבצע אתחול".

עיכוב ההפעלה עדיין קיים, אך הוא לא חוסם את בקשת הרשת, ולכן המשתמש מקבל תוכן מוקדם יותר.

הנה סרטון שבו הוא פועל, שבו ל-Service Worker יש עיכוב מכוון בהפעלה של 500 אלפיות השנייה באמצעות לולאה זמן (time-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.
  • זו בקשת ניווט (שדפדפנים יוצרים כשהם טוענים דפים, כולל 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 כדי שהתשובות במטמון ידעו שהתגובות שלכם שונות.

עכשיו אנחנו יכולים להשתמש בבקשה לטעינה מראש:

// 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) על עבודתם על התכונה הזו, ועל העזרה במאמר הזה. ותודה רבה לכל מי שמעורב במאמץ הסטנדרטי