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

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

เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

การสนับสนุนเบราว์เซอร์

  • 59
  • 18
  • 99
  • 15.4

แหล่งที่มา

สรุป

ปัญหา

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

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

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

การเปิดเครื่อง SW
คำขอการนำทาง

คำขอเครือข่ายล่าช้าเพราะโปรแกรมทำงานของบริการเปิดเครื่อง

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

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

การเปิดเครื่อง SW
คำขอการนำทาง



และเราก็บอกว่า "ใช่ ดูยุติธรรมนะ"

"การโหลดการนำทางล่วงหน้า" เพื่อช่วยเหลือคุณ

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

ส่วนหนึ่งของชุดหนังสือที่ทำงานร่วมกันแบบใหม่