长帧与 Long Task
1. 什么是长帧?
长帧是指一帧内的工作总耗时超过了目标帧率对应的时间预算,导致浏览器错过屏幕刷新时机。
常见帧率对应的单帧预算:
- 60 FPS:
1000 / 60 ≈ 16.7ms - 30 FPS:
1000 / 30 ≈ 33.3ms - 120 FPS:
1000 / 120 ≈ 8.3ms
假设页面以 60 FPS 为目标,一帧中的 JavaScript、样式计算、布局、绘制和合成共耗时 25ms,那么该帧超过了 16.7ms 的预算,属于长帧。用户可能看到动画跳跃、滚动卡顿或交互响应延迟。
需要注意,长帧没有一个脱离场景的固定阈值,它取决于目标刷新率。工程中也可以按业务需要统计超过 16.7ms、33.3ms 或 50ms 的帧比例。
2. 什么是 Long Task?
Long Task 是浏览器任务调度层面的概念:主线程上的一个任务连续执行超过 50ms,就会被 Long Tasks API 识别为长任务。
这里的“任务”可能是:
- 一段同步 JavaScript;
- 一个事件回调;
- 一个定时器回调;
- 脚本解析、编译和执行;
- 同一个任务中触发的样式计算、布局等同步工作。
主线程执行任务期间,通常无法及时处理用户输入、执行 requestAnimationFrame 或进行下一次渲染。一个 100ms 的 Long Task 可能连续阻塞多个 60Hz 刷新机会。
3. 长帧和 Long Task 是什么关系?
Long Task 是长帧的常见原因,但二者不等价,也不是严格的包含关系:
- 长帧关注一整帧是否超过帧预算;
- Long Task 关注单个主线程任务是否超过 50ms。
Long Task 通常会造成长帧
目标为 60 FPS 时,这段任务会错过多次刷新机会,产生明显卡顿。
没有 Long Task 也可能出现长帧
没有任何单项达到 50ms,因此不会被认定为 Long Task;但总耗时已经超过 60 FPS 的 16.7ms 帧预算,仍然会产生长帧。
也可能是多个短任务和渲染工作累计超过预算。因此,只监控 Long Task 会漏掉一部分掉帧问题。
4. 哪些情况容易出现长帧?
(1) JavaScript 计算量过大
- 大数组排序、遍历或 JSON 解析;
- 同一帧更新大量游戏对象;
- 实时物理计算和高密度碰撞检测;
- 一次性同步初始化大量组件或资源;
- 页面从后台恢复后集中补执行事件。
(2) 频繁分配对象和垃圾回收
逐帧创建大量临时对象会推高堆内存。垃圾回收执行时可能暂停 JavaScript,形成周期性长帧。对象池可以减少高频对象的分配和回收,但需要正确重置状态和监听器。
(3) 强制同步布局与布局抖动
在循环中交替修改 DOM,再读取 offsetWidth、getBoundingClientRect() 等几何属性,浏览器为了返回最新数据会被迫立即计算布局。频繁的“写—读—写—读”会造成布局抖动。
(4) 渲染工作过重
- DOM 节点过多;
- 大量 Sprite、粒子或 Spine 骨骼动画;
- 复杂阴影、滤镜、遮罩和透明混合;
- 大面积重绘或过多图层合成;
- 运行时集中进行图片解码和纹理上传。
(5) 高频音频和响应式更新
短时间创建、播放大量音频实例可能增加主线程和混音压力。把游戏对象的逐帧坐标放入 Vue、React 等响应式系统,也可能触发大量依赖更新和渲染工作。
(6) 设备与运行环境变化
低端 CPU/GPU、设备发热降频、内存压力、后台应用竞争、省电模式以及 WebView 实现差异,都可能让原本能在预算内完成的帧变成长帧。
5. 如何监控?
长帧指标
- 平均 FPS;
- P95、P99 帧耗时;
- 超过 16.7ms、33.3ms、50ms 的帧比例;
- 连续长帧数量;
- 低 FPS 会话比例。
P95 帧耗时表示 95% 的帧耗时不超过该值。相比平均 FPS,它更容易暴露少量但影响体验的慢帧。
Long Task 指标
- Long Task 次数;
- Long Task 总阻塞时长;
- 单次最长耗时;
- 每分钟 Long Task 数量;
- 发生 Long Task 时对应的交互和业务场景。
可以通过 PerformanceObserver 监听 Long Task:
Long Tasks API 只能告诉我们主线程何时被长任务阻塞,不能代替完整的逐帧监控。
6. 如何定位?
使用 Chrome DevTools Performance 录制问题场景,重点观察:
- FPS 或 Frames 轨道中是否存在明显慢帧;
- Main 线程是否出现红色标记的 Long Task;
- JavaScript、Style、Layout、Paint、Composite 分别占用多少时间;
- 是否存在频繁 GC;
- 是否存在强制同步布局;
- 长帧发生时游戏对象、DOM 节点和动画数量是否突增;
- 是否发生图片解码、纹理上传或大量资源初始化。
分析时不能只看到 Long Task 就停止,还要继续展开调用栈,定位具体函数以及任务中的脚本、布局和绘制成本。
7. 常见优化方案
- 将长任务拆成多个短任务,分帧或分批执行;
- 将适合的 CPU 密集型计算放入 Web Worker;
- 减少每帧遍历和重复计算,缓存稳定结果;
- 限制同屏对象、粒子和复杂动画数量;
- 使用对象池降低高频分配和 GC 压力;
- DOM 读写分离,避免布局抖动;
- 优先使用
transform和opacity实现动画; - 资源分阶段加载,避免在繁忙帧集中解析和创建;
- 对高频事件、音效和非关键更新进行节流;
- 页面从后台恢复时限制单帧追赶工作量。
拆分任务时需要注意:微任务仍会在浏览器获得下一次渲染机会前被连续清空。把大量工作拆到一串 Promise.then() 中,不一定能让出渲染机会;通常需要通过宏任务、scheduler.yield()、requestAnimationFrame 等方式合理让出主线程,并根据任务优先级选择调度机制。
8. 面试精简回答
长帧是指一帧的总工作时间超过目标帧率的预算,例如 60 FPS 下超过约 16.7ms;Long Task 则是主线程上单个任务连续执行超过 50ms。Long Task 往往会阻塞多个刷新机会,是长帧的重要原因,但两者不等价。即使没有 Long Task,多个短任务加上样式、布局、绘制累计超过 16.7ms,也会产生长帧。因此性能监控既要看 Long Task 次数和阻塞时长,也要看 P95/P99 帧耗时、长帧比例和连续掉帧情况。
9. 常见追问
Long Task 是不是长帧中的一个环节?
可以把它理解为可能占用帧时间的主线程任务,但更严谨地说,它是任务调度层面的指标,并不天然属于某一帧。一个 Long Task 可能横跨多个本应发生的刷新时机,使多帧无法开始渲染。
为什么 Long Task 阈值是 50ms,而 60 FPS 帧预算只有 16.7ms?
因为它们解决的问题不同。16.7ms 来自 60Hz 屏幕的刷新预算,用于判断动画是否流畅;50ms 是 Long Tasks API 用于识别明显主线程阻塞的阈值。一个 25ms 任务不会被标记为 Long Task,但已经可能破坏 60 FPS。
Long Task 和 INP 有什么关系?
Long Task 会让输入事件等待主线程空闲,可能增加交互的输入延迟和处理时间,从而恶化 INP。但 INP 还包括事件处理和下一次绘制呈现的时间,因此不能只通过减少 Long Task 保证 INP 一定良好。

