เพิ่มความเร็วของ Service Worker ด้วยการโหลดการนำทางล่วงหน้า

การโหลดการนําทางล่วงหน้าช่วยให้คุณลดเวลาเริ่มต้นของ Service Worker ได้โดยส่งคําขอพร้อมกัน

การรองรับเบราว์เซอร์

  • Chrome: 59
  • Edge: 18.
  • Firefox: 99
  • Safari: 15.4

แหล่งที่มา

สรุป

ปัญหา

เมื่อคุณไปยังเว็บไซต์ที่ใช้ Service Worker เพื่อจัดการเหตุการณ์การดึงข้อมูล เบราว์เซอร์จะขอการตอบกลับจาก Service Worker ซึ่งเกี่ยวข้องกับการบูต Service Worker (หากยังไม่ได้ทํางาน) และการส่งเหตุการณ์การดึงข้อมูล

เวลาในการบูตจะขึ้นอยู่กับอุปกรณ์และเงื่อนไข โดยปกติจะอยู่ที่ประมาณ 50 มิลลิวินาที บนอุปกรณ์เคลื่อนที่จะอยู่ที่ประมาณ 250 มิลลิวินาที ในบางกรณี (อุปกรณ์ทำงานช้า, CPU ทำงานหนัก) อาจมีเวลามากกว่า 500 มิลลิวินาที อย่างไรก็ตาม เนื่องจาก Service Worker จะทำงานอยู่ในช่วงเวลาระหว่างเหตุการณ์ที่เบราว์เซอร์กำหนดไว้ คุณจึงจะพบความล่าช้านี้ในบางครั้งเท่านั้น เช่น เมื่อผู้ใช้ไปยังเว็บไซต์ของคุณจากแท็บใหม่หรือเว็บไซต์อื่น

เวลาในการบูตไม่มีปัญหาหากคุณตอบกลับจากแคช เนื่องจากประโยชน์ของการข้ามเครือข่ายมากกว่าความล่าช้าในการบูต แต่หากคุณตอบกลับโดยใช้เครือข่าย…

การเริ่มต้นระบบ SW
คำขอการนำทาง

คำขอเครือข่ายล่าช้าเนื่องจากการบูต Service Worker

เรายังคงลดเวลาในการบูตโดยใช้การแคชโค้ดใน V8, ข้าม Service Worker ที่ไม่มีเหตุการณ์การดึงข้อมูล, เปิด Service Worker แบบคาดการณ์ และการเพิ่มประสิทธิภาพอื่นๆ อย่างไรก็ตาม เวลาในการบูตจะมากกว่า 0 เสมอ

Facebook แจ้งให้เราทราบถึงผลกระทบของปัญหานี้และขอวิธีดำเนินการกับคำขอการนำทางแบบคู่ขนาน

การเริ่มต้นระบบ SW
คำขอการนำทาง

การนำทางที่โหลดล่วงหน้าช่วยแก้ปัญหาได้

การโหลดการนําทางล่วงหน้าเป็นฟีเจอร์ที่ช่วยให้คุณพูดได้ว่า "เมื่อผู้ใช้ส่งคําขอการนําทาง 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 ที่ช่วยพัฒนาฟีเจอร์นี้และช่วยเขียนบทความนี้ และขอขอบคุณอย่างยิ่งทุกคนที่มีส่วนร่วมในความพยายามในการทำให้มาตรฐาน