大多数分析、遥测和 Core Web Vitals 分析数据的优先级都低于响应用户,因此您可以放心地推迟发送这些数据,直到用户离开页面。但如果您需要提供实时更新,则例外,因为在这种情况下,时效性至关重要。
若要提供实时分析更新,同时尽可能减少信标,最佳方法是使用 fetchLater() 并搭配 activateAfter 配置选项。这样,您就可以有效地对给定时间范围内发生的所有 Google Analytics 事件进行去抖动和批处理,将其合并为一个信标,即使在超时之前用户离开页面,该信标也能可靠地发送。
如何实现
安排请求:只要有任何相关的分析数据可用,就立即使用数据载荷调用
fetchLater(),并传递activateAfter值。这会将数据排入队列,以便在经过该时间后或在用户提前离开页面时发送。将多个事件分批处理:如果新的分析事件在
activateAfter超时之前发生,请中止之前安排的请求,并再次调用fetchLater()(使用相同的activateAfter值),在单个载荷中包含完整的事件队列。在超时到期时重置事件队列:如果在预定的信标成功发送后(即
fetchLater()结果的activated值为true)发生新的分析事件,则重置事件队列。让浏览器处理其余部分:如果用户在
activateAfter超时之前离开或关闭标签页,浏览器仍会可靠地发送您最近一次fetchLater()调用的载荷。
示例代码
此代码会跟踪网页上的所有 load 和 click 事件,并将 10 秒超时时间内发生的所有事件分批处理。
// Replace with your analytics endpoint.
const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';
// Replace with a time window of your choice. All analytics events that
// occur within this time window will be batched together.
const BATCH_WINDOW = 10 * 1000;
// The maximum number of events to batch. Pick a number that is unlikely
// to overflow the fetchLater() quota for the page.
const MAX_QUEUE_SIZE = 100;
const eventQueue = [];
let fetchLaterResult;
let fetchLaterController;
function trackEvent(eventData) {
// If the previously queued beacon has already been sent, or if the
// max queue size has been met, reset the queue.
if (fetchLaterResult?.activated || eventQueue.length > MAX_QUEUE_SIZE) {
fetchLaterController = null;
fetchLaterResult = null;
eventQueue.length = 0;
}
eventQueue.push(eventData);
// Abort any pending beacons before creating a new one.
if (fetchLaterController) {
fetchLaterController.abort();
}
fetchLaterController = new AbortController();
// Schedule a fetch for the events to be sent when the batch window expires.
// IMPORTANT: wrap the call in a try/catch to handle quota errors.
try {
fetchLaterResult = fetchLater(ANALYTICS_ENDPOINT, {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify(eventQueue),
signal: fetchLaterController.signal,
activateAfter: BATCH_WINDOW,
});
} catch (error) {
// Handle errors as needed.
}
}
// Track page loads.
window.addEventListener('load', () => {
trackEvent({type: 'page_load'});
});
// Track click events.
window.addEventListener('click', (event) => {
trackEvent({type: 'click', target: serializeElement(event.target)});
});
最佳做法
- 务必将
fetchLater()与activateAfter选项搭配使用,以批量处理在短时间内发生的多个分析事件。 - 务必使用
AbortController取消发生新分析事件时待处理的提取。 - 务必检查并确保您没有将过多事件批量处理在一起,从而超出
fetchLater()配额(目前每个来源大约 64KB)。 - 务必将对
fetchLater()的调用封装在try/catch中,以处理配额错误。 - 务必检测
globalThis上是否存在fetchLater(),并针对不支持该 API 的浏览器实施回退策略。 - 请勿在请求正文中使用
ReadableStream对象,否则会出错。
浏览器支持和后备策略
因此,通常需要采用后备策略。
不过,鉴于此 API 具有更高的可靠性和性能优势,如果浏览器支持,则应使用 fetchLater()。
fetchLater() polyfill
请使用以下最小 fetchLater() polyfill,它会在不支持的浏览器中尽可能接近地实现该 API。
此 polyfill 唯一显著的行为差异是,它不会在用户离开页面时发送载荷,而是在页面的 visibilityState 更改为“hidden”时发送载荷,因为这是目前广泛使用的最可靠的会话结束信号。
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;
},
};
};