requestIdleCallback 在渲染间隙发送非核心埋点日志

作者:技术拾光

时间:2026-06-27

浏览:233

requestIdleCallback 并不适合用来保证关键日志的发送,风险较大。它最大的价值在于实现“能发就发、发不了拉倒”的非核心日志补发。即便如此,使用时也必须加上 timeout 保底机制、做好批量合并,并在页面卸载前强制 flush —— 这一整套策略缺一不可。如果用户在你的页面上停留超过

文章目录

requestIdleCallback 并不适合用来保证关键日志的发送,风险较大。它最大的价值在于实现“能发就发、发不了拉倒”的非核心日志补发。即便如此,使用时也必须加上 timeout 保底机制、做好批量合并,并在页面卸载前强制 flush —— 这一整套策略缺一不可。如果用户在你的页面上停留超过 3 秒,建议主动发送日志,而不是等到卸载时再补救。

requestIdleCallback 在渲染间隙发送非核心埋点日志

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 一次,不要把所有希望寄托在页面卸载时的补救上——那太不可控了。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›