requestIdleCallback 在渲染间隙发送非核心埋点日志
requestIdleCallback 并不适合用来保证关键日志的发送,风险较大。它最大的价值在于实现“能发就发、发不了拉倒”的非核心日志补发。即便如此,使用时也必须加上 timeout 保底机制、做好批量合并,并在页面卸载前强制 flush —— 这一整套策略缺一不可。如果用户在你的页面上停留超过
requestIdleCallback 并不适合用来保证关键日志的发送,风险较大。它最大的价值在于实现“能发就发、发不了拉倒”的非核心日志补发。即便如此,使用时也必须加上 timeout 保底机制、做好批量合并,并在页面卸载前强制 flush —— 这一整套策略缺一不可。如果用户在你的页面上停留超过 3 秒,建议主动发送日志,而不是等到卸载时再补救。
requestIdleCallback 什么时候真正可用
它不是一个 Promise,也不保证一定执行。当浏览器忙得不可开交时——比如页面正在疯狂重排重绘,或者 JS 正在执行一个长任务——requestIdleCallback 就会被直接跳过,不发出任何通知。更严峻的是,Chrome 从 120 版本开始默认禁用了这个 API(需手动开启实验标志),而 Safari 和 Firefox 早已移除。因此,不能把它当作可靠定时器来用。它最适合的角色,是去做那种“能发出去也好,发不出去也对主流程没影响”的日志补发。
埋点日志必须带 timeout 保底机制
仅靠 requestIdleCallback 单独发送日志,可靠性不足。用户一切页、一刷新或关标签时,浏览器可能没有空闲时间来触发回调,日志容易漏发。必须配合 setTimeout 或页面卸载钩子做兜底。以下为示例代码:
function sendLogWithIdle(log) { const timeoutId = setTimeout(() => { sendLogNow(log); // 立即发,不等 idle }, 2000); requestIdleCallback(() => { clearTimeout(timeoutId); sendLogNow(log); }, { timeout: 2000 });}
- 这里的
timeout: 2000是关键参数,如果不传,即使外层有 setTimeout,回调也可能永远不进 idle 队列。 clearTimeout必须放在 idle 回调里,避免 idle 触发后,timeout 分支重复发送。- 需要注意:Firefox 和 Safari 不支持
timeout选项,因此在那些浏览器上,完全依赖外层的setTimeout来模拟保底逻辑。
批量合并日志再发,别每个埋点都 call 一次
如果每个埋点都调用一次 requestIdleCallback,不仅无法优化性能,反而会堆积大量微任务,增加调度开销。正确的做法是先将日志暂存,每隔 500ms 合并一次,然后用单次 idle 回调一次性发出:
let pendingLogs = [];let idleHandle = null;function queueLog(log) { pendingLogs.push(log); if (!idleHandle) { idleHandle = requestIdleCallback(flushLogs, { timeout: 1000 }); }}function flushLogs({ didTimeout }) { if (pendingLogs.length === 0) return; if (didTimeout || pendingLogs.length >= 10) { sendBatch(pendingLogs); pendingLogs = []; } else { // 还没到量,再等一会儿 idleHandle = requestIdleCallback(flushLogs, { timeout: 500 }); }}
didTimeout参数很关键,它告诉浏览器实在腾不出空闲,此时应立即发出积压日志,防止越堆越多。- 同时限制单次发送最多 10 条,避免请求体过大阻塞主线程。
- 另有一个小陷阱:不要在
flushLogs中直接递归调用requestIdleCallback,容易撑爆调用栈。用全局变量控制是否已挂起 idle 回调,是最稳妥的做法。
卸载前强制 flush,但别用 beforeunload
在页面卸载前强制发送日志,思路正确,但千万不要用 beforeunload 事件。Chrome 在该事件中禁止发起网络请求,会静默丢弃。正确的做法是监听 visibilitychange 和 pagehide 事件:
- 当检测到
document.visibilityState === 'hidden'时,立即 flush 所有日志。 pagehide事件比unload触发更早,且允许 fetch 发送请求,只需加上keepalive: true属性。fetch(url, { method: 'POST', body: JSON.stringify(logs), keepalive: true })几乎是唯一能在页面销毁前可靠发出请求的途径。
不过,不同浏览器对 keepalive 的支持程度不同,Safari 仍可能截断请求。因此最保险的做法是:用户在你的页面上停留超过 3 秒,就主动 flush 一次,不要把所有希望寄托在页面卸载时的补救上——那太不可控了。
