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










