JavaScript中宏任务统计的是调度延迟而非执行耗时,即从调度到开始执行的时间差,通过performance.now()在调度和回调入口打点计算,并用百分位数(如P95、P99)分析延迟分布。

在 JavaScript 中,宏任务(如 setTimeout、setInterval、I/O 回调、UI 渲染后回调等)本身没有直接暴露执行耗时的 API,但你可以通过手动打点 + 时间采样 + 百分位计算的方式,在宏观层面统计「某类宏任务实际执行所花费的时间分布」。关键不是测量单个宏任务内部耗时(那属于微任务或同步代码范畴),而是统计「从宏任务被调度到它真正开始执行之间的时间延迟」,即宏任务排队等待时间(也称调度延迟),这更能反映事件循环压力。
为什么统计的是“调度延迟”而非“执行耗时”
JavaScript 引擎不提供宏任务执行体的自动计时钩子。你无法像 PerformanceObserver 监听 paint 或 longtask 那样监听 setTimeout 回调的执行起止。但你可以控制宏任务的入口:在调度时记录时间,在回调开始时再取一次时间,二者之差就是该次宏任务在队列中等待的时间——这个值对性能分析极有价值,尤其在高负载场景下能暴露事件循环阻塞问题。
用 performance.now() 手动打点统计延迟
利用高精度时间戳(performance.now())在调度和执行两个节点打点:
- 调度时(如调用
setTimeout前)记录enqueueTime - 回调执行第一行记录
startTime - 差值
delay = startTime - enqueueTime即为该次宏任务的排队延迟
示例:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
const delays = [];
<p>function scheduleWithTrace(cb, delayMs = 0) {
const enqueueTime = performance.now();
setTimeout(() => {
const startTime = performance.now();
const delay = startTime - enqueueTime;
delays.push(delay);
cb();
}, delayMs);
}</p><p>// 使用
scheduleWithTrace(() => console.log('done'), 0);</p>收集足够样本后计算百分位(如 P95、P99)
百分位数需基于有序数据集。建议:
- 限制样本数量(如最多保留 1000 条),避免内存泄漏
- 使用快速排序或内置
Array.prototype.sort()排序(小数据量够用) - P95 公式:
index = Math.floor(delays.length * 0.95),取排序后对应位置值
简易实现:
function getPercentile(arr, p) {
if (arr.length === 0) return 0;
const sorted = [...arr].sort((a, b) => a - b);
const index = Math.floor(sorted.length * p);
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
}
<p>console.log('P95 delay:', getPercentile(delays, 0.95), 'ms');
console.log('P99 delay:', getPercentile(delays, 0.99), 'ms');</p>进阶:结合 requestIdleCallback 或 PerformanceObserver 做轻量监控
若想长期、低侵入地监控,可配合:
-
requestIdleCallback:在浏览器空闲时段批量上报延迟统计(避免影响主线程) -
PerformanceObserver(type: 'longtask'):捕获 > 50ms 的长任务,间接反映宏任务可能面临的阻塞环境 - 自定义宏任务包装器(如封装所有
setTimeout调用)统一埋点,适合 SDK 或框架层集成
注意:不要在生产环境高频采集(如每毫秒一个 setTimeout),应按需采样(例如仅对关键业务逻辑的宏任务打点)。

















