วัดผลการวิเคราะห์และข้อมูลการตรวจวัดระยะไกลแบบเต็มเซสชันได้อย่างน่าเชื่อถือ

ฟีเจอร์บนเว็บ: fetchLater AbortController และ AbortSignal

หากต้องการติดตามข้อมูลวิเคราะห์, Core Web Vitals และข้อมูลการตรวจวัดระยะไกลที่ครอบคลุมการเข้าชมหน้าเว็บของผู้ใช้ทั้งหมด (ไม่ใช่แค่จนถึงการโหลดหน้าเว็บ) อย่างน่าเชื่อถือ ให้ใช้ fetchLater() API

API fetchLater() เป็นวิธีที่น่าเชื่อถือที่สุดในการส่งข้อมูลไปยังเซิร์ฟเวอร์ในกรณีที่ไม่จำเป็นต้องมีการตอบกลับและเวลาในการนำส่งไม่เร่งด่วน ซึ่งใช้กับข้อมูลต่างๆ เช่น ข้อมูลวิเคราะห์ผู้ใช้ การวัดและส่งข้อมูลทางไกล การติดตามข้อผิดพลาด และเมตริกประสิทธิภาพ เช่น Core Web Vitals

เทคนิคเก่าๆ เช่น การสร้างพิกเซล <img> ใน Listener เหตุการณ์ unload นั้นไม่น่าเชื่อถือ (โดยเฉพาะในอุปกรณ์เคลื่อนที่) และอาจส่งผลเสียต่อประสิทธิภาพ (โดยทำให้หน้าเว็บไม่มีสิทธิ์ใช้ bfcache)

วิธีการนำไปใช้งาน

  1. กำหนดเวลาคำขอ: ทันทีที่ข้อมูลที่เกี่ยวข้องพร้อมใช้งาน ให้เรียกใช้ fetchLater() พร้อมเพย์โหลดข้อมูล ซึ่งจะจัดคิวข้อมูลเพื่อส่งในภายหลัง

  2. อัปเดตเพย์โหลดตามต้องการ: หากผู้ใช้สร้างข้อมูลเพิ่มเติมหรือสถานะมีการเปลี่ยนแปลง ให้ยกเลิกคำขอที่กำหนดเวลาไว้ก่อนหน้านี้ แล้วเรียกใช้ fetchLater() อีกครั้งพร้อมสแนปชอตที่อัปเดตอย่างสมบูรณ์

  3. ให้เบราว์เซอร์จัดการส่วนที่เหลือ: เมื่อผู้ใช้ออกจากหน้าเว็บหรือปิดแท็บ เบราว์เซอร์จะส่งเพย์โหลดจากการเรียก fetchLater() ล่าสุดได้อย่างน่าเชื่อถือ

โค้ดตัวอย่าง

โค้ดนี้จะวัดระยะเวลาเซสชันของการเข้าชมหน้าเว็บของผู้ใช้โดยใช้ fetchLater() เพื่อจัดคิวบีคอนใหม่ทุกๆ 10 วินาทีพร้อมระยะเวลาเซสชันที่อัปเดต

const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

const sessionData = {
  duration: 0,
  id: crypto.randomUUID(),
};

let fetchLaterController = null;

function queueBeacon() {
  // Abort any pending beacons before creating a new one.
  if (fetchLaterController) {
    fetchLaterController.abort();
  }
  fetchLaterController = new AbortController();

  // Update session duration to the current page time.
  sessionData.duration = performance.now();

  // Schedule a fetch for the data payload to be sent later.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(sessionData),
      signal: fetchLaterController.signal,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Update the session data and queue a new beacon every 10 seconds.
setInterval(queueBeacon, 10000);

แนวทางปฏิบัติแนะนำ

  • ควรใช้ fetchLater() เพื่อส่งข้อมูลไปยังเซิร์ฟเวอร์ในสถานการณ์ที่ไม่จำเป็นต้องมีการตอบกลับ และไม่จำเป็นต้องส่งข้อมูลทันที
  • ควรใช้ AbortController เพื่อยกเลิกการดึงข้อมูลที่รอดำเนินการในกรณีที่อาจต้องอัปเดตข้อมูลก่อนที่ผู้ใช้จะออกจากหน้าเว็บ
  • ควรลดขนาดเพย์โหลดเพื่อหลีกเลี่ยงการใช้เกินโควต้า (ปัจจุบันอยู่ที่ประมาณ 64 KB ต่อต้นทาง)
  • ควรรวมการเรียกไปยัง fetchLater() ไว้ใน try/catch เพื่อจัดการข้อผิดพลาดเกี่ยวกับโควต้า
  • ควรใช้ฟีเจอร์ตรวจหาการมีอยู่ของ fetchLater() ใน globalThis และใช้กลยุทธ์สำรองสำหรับเบราว์เซอร์ที่ไม่รองรับ API
  • อย่าใช้ออบเจ็กต์ ReadableStream สำหรับเนื้อหาคำขอ เนื่องจากจะทำให้เกิดข้อผิดพลาด

การรองรับเบราว์เซอร์และกลยุทธ์สำรอง

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

กลยุทธ์สำรองที่แนะนำคือการใช้ Polyfill ด้านล่าง ซึ่งจะจัดการเบราว์เซอร์ที่ไม่รองรับภายในโดยใช้ fetch() กับ keepalive หรือ navigator.sendBeacon() โค้ดของคุณต้องเรียก fetchLater() โดยตรง และห้ามเรียก fetch(), sendBeacon() หรือ API บีคอนอื่นๆ ด้วยตนเอง

fetchLater() polyfill

ใช้ Polyfill fetchLater() ขั้นต่ำต่อไปนี้ ซึ่งจะใช้ API อย่างใกล้เคียงที่สุดในเบราว์เซอร์ที่ไม่รองรับ

ความแตกต่างด้านลักษณะการทำงานที่สำคัญเพียงอย่างเดียวของ Polyfill นี้คือการใช้ visibilitychange เพื่อตรวจจับเมื่อผู้ใช้ออกจากหน้าเว็บ แทนที่จะอาศัยการจัดการการยกเลิกการโหลดแบบเนทีฟของเบราว์เซอร์ นี่เป็นรายละเอียดการใช้งานภายใน โค้ดของคุณไม่จำเป็นต้องรอฟัง visibilitychange หรือเหตุการณ์ในวงจรของหน้าเว็บอื่นๆ เพียงเรียกใช้ fetchLater() แล้ว Polyfill จะจัดการการนำส่งให้

globalThis.fetchLater ??= function fetchLater(url, init = {}) {
  let timeoutHandle;
  let activated = false;

  function sendNow() {
    if (!(init.signal && init.signal.aborted)) {
      // Use fetch keepalive if the browser supports it or if custom fetch
      // parameters are specified (e.g. custom headers or methods).
      // Otherwise fall back to `navigator.sendBeacon()`.
      if (
        'keepalive' in Request.prototype ||
        init.method !== 'POST' ||
        init.headers
      ) {
        fetch(url, Object.assign({}, init, {keepalive: true}));
        activated = true;
      } else {
        activated = navigator.sendBeacon(url, init.body);
      }
    }
    destroy();
  }

  function destroy() {
    document.removeEventListener('visibilitychange', sendNow);
    clearTimeout(timeoutHandle);
  }

  if (document.visibilityState === 'hidden') {
    // If the beacon was created while the page is already hidden, send data
    // ASAP but wait until the next microtask to allow all sync code to run.
    queueMicrotask(sendNow);
  } else {
    document.addEventListener('visibilitychange', sendNow);

    if (typeof init.activateAfter === 'number' && init.activateAfter >= 0) {
      timeoutHandle = setTimeout(sendNow, init.activateAfter);
    }
  }

  if (init.signal) {
    init.signal.addEventListener('abort', destroy);
  }

  return {
    get activated() {
      return activated;
    },
  };
};