前端卡顿监控的最后一块拼图:我把 STW Sentinel 接进真实业务,终于分清了 React 卡顿和 V8 GC
0. 开场:页面卡了,老板只问一句话
用户说页面卡。产品说转化掉了。后端说接口很快。前端打开 DevTools,只看到一坨 Long Task。
于是所有人开始猜:是不是 React 组件太多?是不是列表没虚拟滚动?是不是 CSS layout thrashing?是不是 Chrome 又抽风?
传统前端监控只能告诉你"卡了",但很难告诉你"是谁让世界暂停了"。
这里顺手点名 rAF、PerformanceObserver、Long Task、web-vitals 的局限:它们都在主线程语境里观察主线程。
1. 为什么传统卡顿监控会失明?
核心论点:如果监控代码和业务代码在同一个线程,它们会一起死。
1.1 requestAnimationFrame
能看到帧间隔变大,但它自己也被主线程调度影响。它像是在心脏停跳后醒来补记一笔:"刚才好像断片了 700ms。"
1.2 Long Task API
能看到超过 50ms 的主线程长任务,但它更擅长记录 JS 执行和任务阻塞,不等于能精确切开 V8 STW 的瞬间。
1.3 DevTools Performance
适合开发环境复盘,但不适合生产环境持续采样。用户现场不会帮你开 DevTools。
这一节的结尾要引出:我们需要一个不坐在主线程里的观察者。
2. STW Sentinel 的定位:不是替代 web-vitals,而是补上黑匣子
这节非常关键。不要把 stw-sentinel 写成"吊打所有监控"。更高级的写法是:
web-vitals 看用户体验结果,Long Task 看主线程任务,STW Sentinel 看主线程之外的物理心跳。
| 监控手段 | 能回答的问题 | 盲区 |
|---|---|---|
| web-vitals | 用户体验是否变差 | 很难解释底层原因 |
| Long Task | 主线程是否被长任务占用 | 不一定能区分业务 JS、Layout、GC |
| rAF delta | 帧是否断了 | 采样者自己也会被卡住 |
| STW Sentinel | 主线程冻结期间外部时间是否仍稳定流逝 | 需要 COOP/COEP 与 AudioWorklet 环境 |
STW Sentinel 不是性能监控的全部,而是卡顿归因链路里缺失的那颗钉子。
3. 生产接入架构:不要只 console.warn,要做事件归因
不要只记录 deltaMs,要记录上下文。
import { STWSentinel } from 'stw-sentinel'
const sentinel = new STWSentinel({
thresholdMs: 10,
onSpike: (deltaMs, entry) => {
reportSTW({
deltaMs,
timestamp: performance.now(),
route: location.pathname,
visibility: document.visibilityState,
userAgent: navigator.userAgent,
recentAction: getLastUserAction(),
recentLongTasks: getRecentLongTasks(),
memory: getMemorySnapshotSafely(),
})
},
})建议上报字段:
| 字段 | 作用 |
|---|---|
deltaMs | STW 或调度尖峰长度 |
route | 哪个页面最容易卡 |
recentAction | 是否发生在点击、输入、滚动之后 |
recentLongTasks | 和 Long Task 做交叉验证 |
visibilityState | 排除后台标签页误判 |
deviceMemory | 低端设备分层 |
hardwareConcurrency | CPU 核心数分层 |
browser | Chrome / Edge / Safari 差异 |
releaseVersion | 对应前端版本回归 |
4. 卡顿归因矩阵:如何判断是谁的锅?
情况 A:Long Task 高,STW 不高
结论倾向:业务 JS、React render、同步计算、JSON parse、大循环、第三方 SDK。
处理方向:
- 拆任务
- useMemo / memo
- 虚拟列表
- Web Worker
- 减少同步 JSON parse
- 延迟第三方 SDK 初始化
情况 B:Long Task 高,STW 也高
结论倾向:业务代码制造了内存压力,触发 V8 GC/STW。
典型场景:
- 短时间创建大量对象
- 大数组频繁 map/filter/reduce
- 虚拟 DOM 大规模重建
- 不可控缓存膨胀
- 频繁 JSON.parse/stringify
- 大对象深拷贝
情况 C:STW 高,但 Long Task 不明显
结论倾向:传统主线程观测没抓到完整现场,或者 GC 停顿发生在监控盲区。
处理方向:
- 看内存分配曲线
- 看路由切换前后的对象增长
- 看第三方脚本
- 看是否存在大规模临时对象
情况 D:rAF 掉帧,但 STW 稳定
结论倾向:渲染、布局、合成、GPU、CSS、图片解码等问题。
处理方向:
- 查 Layout Thrashing
- 查 forced reflow
- 查大面积 repaint
- 查 CSS filter/backdrop-filter
- 查图片解码与 canvas
5. 一个真实案例:React 页面卡顿,最后不是 React 的锅
案例结构:
- 页面:大型数据看板
- 现象:切换筛选条件时偶发 300ms 卡顿
- 传统监控:Long Task 记录不稳定
- 怀疑对象:React 组件重渲染
- 接入 STW Sentinel:发现卡顿前后出现 120ms STW spike
- 继续排查:筛选逻辑中大量 JSON 深拷贝 + 临时对象创建
- 修复:结构共享、缓存复用、减少中间数组
- 结果:STW spike 从 120ms 降到 18ms,交互延迟下降
我们不是让 V8 不 GC,而是减少把 V8 逼到 Stop-The-World 的概率。
6. 阈值怎么设:不要迷信 16.6ms
- 5ms 以下:通常不需要报警,但可以采样
- 10ms:适合开发环境敏感阈值
- 16.6ms:一帧预算
- 50ms:Long Task 标准线
- 100ms+:用户明显感知
- 300ms+:交互断裂
- 700ms+:事故现场
推荐策略:
- 开发环境:thresholdMs = 5~10
- 灰度环境:thresholdMs = 10~20
- 生产环境:分层采样,重点记录 50ms+ 和 100ms+
阈值不是物理真理,是业务容忍度。 游戏、音频、交易、编辑器、看板、后台管理系统的阈值不一样。
7. 生产环境注意事项:这把武器有保险
7.1 COOP/COEP 会影响资源加载
很多人配置 Cross-Origin-Embedder-Policy: require-corp 后,会发现第三方图片、脚本、iframe、CDN 资源出问题。
建议:
- 先在实验域名或灰度域名启用
- 检查第三方资源 CORP/CORS
- 避免直接在全站裸上
7.2 AudioContext 必须用户手势后启动
建议:
- 在用户第一次点击、滚动、输入后懒启动
- 不要在页面加载时强行初始化
- 对后台标签页降采样或暂停
7.3 不要全量上报所有心跳
生产环境只上报异常尖峰和少量采样窗口。
- 正常心跳留在本地环形缓冲区
- 超过阈值才 drain + report
- 同一 session 做限流
7.4 兼容性要诚实
不是所有浏览器、所有嵌入环境都适合跑这套东西。尤其是微信内置浏览器、企业内嵌 WebView、老 Safari、跨域资源复杂的老项目,都要给降级策略。
8. 升维:前端性能监控要从"指标"走向"物理观测"
过去我们用指标描述用户体验:LCP、FID、INP、CLS。现在我们还需要一层更底层的东西:物理心跳。
因为当主线程停止呼吸时,所有跑在主线程里的监控都会变成事后回忆。
STW Sentinel 不是为了证明 AudioWorklet 有多酷,而是为了把前端卡顿从玄学、猜测和甩锅,拉回到可观测、可归因、可复现的工程系统里。
如果你只想试一下,5 行代码接入。
npm install stw-sentinel如果你想定位真实业务卡顿,请记录上下文、交叉 Long Task、按路由和设备聚合。
页面卡了不可怕,可怕的是你不知道它为什么卡。
🔗 相关文章