performance.measure仅记录时间差,需手动组合mark/measure/判断/dom更新四步实现监控与补偿;标记点须紧贴交互入口出口,补偿逻辑应前置且与渲染帧对齐。

Performance.measure 本身不触发任何视觉反馈,它只记录两个 performance.mark() 之间的时间差。要实现“监控耗时 + 触发补偿”,必须手动组合标记、测量、判断与 DOM 更新三步,且需避开主线程阻塞导致的反馈延迟。
用 performance.mark 和 performance.measure 捕获真实点击到响应完成的耗时
用户感知的“卡顿”往往不是从 click 事件开始算,而是从他松开手指(或释放鼠标)那一刻起——但浏览器在事件回调里才真正开始处理。所以标记点必须紧贴交互入口和出口:
-
performance.mark('ui_interaction_start')应放在事件监听器最开头,比如button.addEventListener('click', () => { performance.mark('ui_interaction_start'); ... }) -
performance.mark('ui_interaction_end')必须放在所有同步逻辑结束、且 DOM 已更新(或requestAnimationFrame回调内)之后,否则测出的是“逻辑完成时间”,不是“用户可见响应时间” - 不要在异步回调(如
setTimeout或fetch.then)里直接打end标记——除非你明确想测网络请求耗时,而不是交互延迟
判断是否超阈值并决定是否启动视觉补偿
100ms 是多数用户产生“卡顿感”的临界点,但实际中建议按场景分级:16ms(1帧)用于动画类操作,100ms 用于按钮点击,300ms 用于列表下拉加载。补偿逻辑不能写在测量之后立刻执行,否则可能被长任务打断:
- 用
setTimeout(() => { /* 补偿逻辑 */ }, 0)或queueMicrotask()把补偿判断移出当前调用栈,避免阻塞渲染 - 测量后立即读取
performance.getEntriesByName('interaction_latency', 'measure'),注意返回的是数组,取[0].duration - 若耗时 >
100,且尚未显示 loading 状态(需用布尔变量防重复触发),才执行骨架屏、按钮禁用、旋转图标等视觉反馈
为什么不能只靠 PerformanceObserver 监听 measure 类型
PerformanceObserver 对 measure 类型的支持在 Safari 和部分旧版 Chrome 中不可靠——它不会自动触发,也不保证回调时机。更关键的是:它无法区分你测的是“用户点击”还是“内部计算”,缺乏上下文关联:
- 监听
measure条目会捕获所有performance.measure()调用,包括工具库自动生成的,噪音大 - 你无法在 observer 回调里拿到触发该 measure 的 DOM 元素或事件对象,没法精准控制对应 UI
- 真正需要补偿的,是那个具体按钮或卡片的反馈状态,不是全局指标——所以必须在业务逻辑里做判断,而非依赖 observer
视觉补偿容易被忽略的渲染时机问题
即使你正确设置了 button.disabled = true 或 el.classList.add('loading'),如果这些操作发生在长任务末尾,浏览器可能已错过下一帧渲染机会,导致“看起来没反应”。真正有效的做法是:
- 在
mark('ui_interaction_start')后**立刻**更新 UI(比如加 loading class),而不是等测量完再更新 - 把耗时测量当作“验证”:如果最终
duration ,再用 <code>requestAnimationFrame移除 loading 状态,避免闪动 - 避免在补偿逻辑里执行 layout 强制同步(如读取
offsetHeight),否则会额外增加一帧延迟
关键点始终是:测量只是诊断手段,补偿动作必须前置、轻量、与渲染帧对齐。别让性能监控代码本身变成新的性能瓶颈。










