如何用 requestIdleCallback 在渲染间隙处理非核心埋点日志发送

作者:袖梨 2026-06-28
requestIdleCallback 不可靠,仅适用于非关键日志补发,必须搭配 timeout 保底、批量合并、卸载前强制 flush(用 pagehide + keepalive)及停留超3秒主动发送。

requestIdleCallback 什么时候真正可用

它不是 Promise,也不保证一定执行——浏览器忙的时候可能一直不调用,比如页面正在重排重绘、JS 正在执行长任务,requestIdleCallback 就会被跳过。Chrome 从 120 版本起还默认禁用了该 API(需手动开启实验标志),而 Safari 和 Firefox 早已移除支持。所以别把它当「可靠定时器」用,只适合做「能发就发、不发也无妨」的日志补发。

埋点日志必须带 timeout 保底机制

requestIdleCallback 单独发日志,大概率会漏发:用户切页、刷新、关标签前 idle 时间根本来不及触发。必须搭配 setTimeout 或页面卸载钩子兜底:

function sendLogWithIdle(log) {  const timeoutId = setTimeout(() => {    sendLogNow(log); // 立即发,不等 idle  }, 2000);  requestIdleCallback(() => {    clearTimeout(timeoutId);    sendLogNow(log);  }, { timeout: 2000 });}
  • timeout: 2000 是关键参数,否则即使设了 timeout,回调也可能永远不进
  • clearTimeout 要放在 callback 里,避免 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,容易栈溢出;用变量控制是否已挂起

卸载前强制 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 一次,不全指望卸载时补救。

相关文章

精彩推荐