requestIdleCallback 不可靠,仅适用于非关键日志补发,必须搭配 timeout 保底、批量合并、卸载前强制 flush(用 pagehide + keepalive)及停留超3秒主动发送。
它不是 Promise,也不保证一定执行——浏览器忙的时候可能一直不调用,比如页面正在重排重绘、JS 正在执行长任务,requestIdleCallback 就会被跳过。Chrome 从 120 版本起还默认禁用了该 API(需手动开启实验标志),而 Safari 和 Firefox 早已移除支持。所以别把它当「可靠定时器」用,只适合做「能发就发、不发也无妨」的日志补发。
靠 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 分支重复发送timeout 选项,得靠外层 setTimeout 模拟频繁调用 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 表示浏览器实在腾不出空,此时应立即发,避免积压flushLogs 里直接递归调用 requestIdleCallback,容易栈溢出;用变量控制是否已挂起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 一次,不全指望卸载时补救。