可靠地衡量完整会话分析和遥测数据

如需可靠地跟踪涵盖用户对网页的整个访问过程(而不仅仅是网页加载过程)的分析、Core Web Vitals 和遥测数据,请使用 fetchLater() API。

在不需要响应且传送时间不紧急的情况下,fetchLater() API 是向服务器发送数据的最可靠方式,适用于用户分析、遥测、错误跟踪和性能指标(例如 Core Web Vitals)等数据。

众所周知,在 unload 事件监听器中创建 <img> 像素等旧技术并不可靠(尤其是在移动设备上),并且可能会对性能产生负面影响(导致网页不符合 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 取消待处理的提取操作
  • 务必尽量减小载荷大小,以免超出配额(目前每个源大约 64KB)。
  • 务必将对 fetchLater() 的调用封装在 try/catch 中,以处理配额错误。
  • 务必检测 globalThis 上是否存在 fetchLater(),并针对不支持该 API 的浏览器实施回退策略。
  • 请勿在请求正文中使用 ReadableStream 对象,否则会出错。

浏览器支持和后备策略

如果 fetchLater() 未达到基准目标,则需要采用后备策略。不过,鉴于此 API 具有更高的可靠性和性能优势,如果浏览器支持,则应使用 fetchLater()。

建议的后备策略是使用以下 polyfill,它会在内部使用 fetch() 搭配 keepalive 或 navigator.sendBeacon() 来处理不支持的浏览器。您自己的代码必须直接调用 fetchLater(),切勿自行调用 fetch()、sendBeacon() 或其他信标 API。

fetchLater() polyfill

请使用以下最小 fetchLater() polyfill,它会在不支持的浏览器中尽可能接近地实现该 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;
    },
  };
};