长帧与 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 通常会造成长帧

JavaScript Long Task:80ms
样式、布局和绘制:5ms
总阻塞时间:85ms

目标为 60 FPS 时,这段任务会错过多次刷新机会,产生明显卡顿。

没有 Long Task 也可能出现长帧

JavaScript:10ms
样式计算:4ms
布局:3ms
绘制与合成:4ms
合计:21ms

没有任何单项达到 50ms,因此不会被认定为 Long Task;但总耗时已经超过 60 FPS 的 16.7ms 帧预算,仍然会产生长帧。

也可能是多个短任务和渲染工作累计超过预算。因此,只监控 Long Task 会漏掉一部分掉帧问题。

4. 哪些情况容易出现长帧?

(1) JavaScript 计算量过大

  • 大数组排序、遍历或 JSON 解析;
  • 同一帧更新大量游戏对象;
  • 实时物理计算和高密度碰撞检测;
  • 一次性同步初始化大量组件或资源;
  • 页面从后台恢复后集中补执行事件。

(2) 频繁分配对象和垃圾回收

逐帧创建大量临时对象会推高堆内存。垃圾回收执行时可能暂停 JavaScript,形成周期性长帧。对象池可以减少高频对象的分配和回收,但需要正确重置状态和监听器。

(3) 强制同步布局与布局抖动

在循环中交替修改 DOM,再读取 offsetWidthgetBoundingClientRect() 等几何属性,浏览器为了返回最新数据会被迫立即计算布局。频繁的“写—读—写—读”会造成布局抖动。

(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:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log({
      startTime: entry.startTime,
      duration: entry.duration,
    });
  }
});

observer.observe({ type: 'longtask', buffered: true });

Long Tasks API 只能告诉我们主线程何时被长任务阻塞,不能代替完整的逐帧监控。

6. 如何定位?

使用 Chrome DevTools Performance 录制问题场景,重点观察:

  1. FPS 或 Frames 轨道中是否存在明显慢帧;
  2. Main 线程是否出现红色标记的 Long Task;
  3. JavaScript、Style、Layout、Paint、Composite 分别占用多少时间;
  4. 是否存在频繁 GC;
  5. 是否存在强制同步布局;
  6. 长帧发生时游戏对象、DOM 节点和动画数量是否突增;
  7. 是否发生图片解码、纹理上传或大量资源初始化。

分析时不能只看到 Long Task 就停止,还要继续展开调用栈,定位具体函数以及任务中的脚本、布局和绘制成本。

7. 常见优化方案

  • 将长任务拆成多个短任务,分帧或分批执行;
  • 将适合的 CPU 密集型计算放入 Web Worker;
  • 减少每帧遍历和重复计算,缓存稳定结果;
  • 限制同屏对象、粒子和复杂动画数量;
  • 使用对象池降低高频分配和 GC 压力;
  • DOM 读写分离,避免布局抖动;
  • 优先使用 transformopacity 实现动画;
  • 资源分阶段加载,避免在繁忙帧集中解析和创建;
  • 对高频事件、音效和非关键更新进行节流;
  • 页面从后台恢复时限制单帧追赶工作量。

拆分任务时需要注意:微任务仍会在浏览器获得下一次渲染机会前被连续清空。把大量工作拆到一串 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 一定良好。