WebGL与页面动画渲染性能优化 - 第2节

性能瓶颈分析:CPU/GPU负载对动画的影响

做动画优化,说白了就是跟两个东西打交道:CPU 和 GPU。我见过太多人一上来就调 CSS 属性,结果帧率纹丝不动。为什么?因为你根本没找到瓶颈在哪。

我个人习惯,拿到一个卡顿页面,先不急着改代码。先问自己三个问题:

  • 是 CPU 算不过来,还是 GPU 画不过来?
  • 主线程被什么东西堵住了?
  • 每一帧里,浏览器到底在忙什么?

这三个问题搞清楚了,优化方向自然就出来了。

CPU 负载:谁在抢你的计算资源?

CPU 负责的事情很杂。JavaScript 执行、样式计算、布局、绘制准备,全在它身上。你想想看,如果每一帧里 JS 要跑 30ms,那剩下 16ms 根本不够浏览器做其他事。

我在项目中遇到过这样一个案例:一个列表动画,每帧都要重新计算所有元素的位置。代码逻辑没问题,但数据量一上来,CPU 直接飙到 90%。帧率从 60fps 掉到 15fps。后来改成只计算可视区域内的元素,CPU 负载瞬间降下来。

CPU 负载过高的典型表现:

  • DevTools Performance 面板里,Scripting 和 Style 阶段耗时过长
  • 帧率波动大,偶尔掉到 30fps 以下
  • 页面滚动时明显卡顿,尤其是列表或大量 DOM 操作

GPU 负载:合成层的陷阱

GPU 擅长的是合成和纹理处理。你把元素提升为合成层(比如用 will-change: transform),浏览器就会把它交给 GPU 去处理。听起来很美好,对吧?

但这里有个坑。每个合成层都需要占用 GPU 内存。层太多,GPU 显存吃紧,反而会拖慢性能。我曾经见过一个页面,为了追求流畅,给每个卡片都加了 will-change: transform。结果呢?GPU 内存爆了,动画比原来还卡。

注意:合成层不是越多越好。一般建议控制在 10 层以内。超出这个范围,GPU 的并行优势会变成负担。

主线程阻塞与帧丢失

浏览器渲染一帧,大概走这么几步:

  1. JavaScript 执行(如果有事件或动画回调)
  2. 样式计算(Style)
  3. 布局(Layout)
  4. 绘制(Paint)
  5. 合成(Composite)

这整个过程,大部分都在主线程上跑。主线程一旦被堵住,后面的步骤全得等。帧就丢了。

为什么会丢帧?说白了,就是主线程忙不过来。比如你有一个 setInterval 每 16ms 执行一次,但里面跑了个 50ms 的同步任务。那这一帧肯定超时,下一帧也得往后推。

我的经验:用 requestAnimationFrame 代替 setInterval 做动画。前者会跟浏览器的渲染循环同步,不会在帧中间插队。后者可不管这些,该卡还是卡。

长任务:帧率的隐形杀手

Chrome DevTools 里有个概念叫「长任务」(Long Task)。凡是主线程执行超过 50ms 的任务,都会被标记出来。50ms 什么概念?差不多 3 帧的时间。你想想看,一个任务吃掉 3 帧,用户能感觉到卡顿吗?当然能。

我处理过一个长任务问题:一个数据可视化页面,每次数据更新都要重新计算整个图表。数据量大概 5000 个点,计算一次要 80ms。用户拖动滑块时,每帧都在触发计算,帧率直接掉到 12fps。

解决方案是把计算拆成小块,用 requestIdleCallback 分散到空闲时间去做。或者用 Web Worker 扔到后台线程。总之,别让主线程一个人扛。

任务类型 耗时范围 对帧率的影响 推荐处理方式
短任务 < 5ms 几乎无影响 正常执行
中任务 5ms - 30ms 偶尔掉帧 考虑拆分或优化
长任务 > 50ms 明显卡顿 必须拆分或异步化

使用 Chrome DevTools 进行性能录制与分析

工具用得对,问题找得快。Chrome DevTools 的 Performance 面板,是我最常用的调试工具。没有之一。

录制一段性能数据

操作很简单:打开 DevTools → Performance → 点那个圆点开始录制 → 在页面上操作(比如滚动、点击、触发动画)→ 再点一下停止。

录制完成后,你会看到一条时间轴。上面密密麻麻的色块,就是浏览器每一帧在干什么。

  • 绿色: 绘制(Paint)
  • 紫色: 样式计算(Style)
  • 黄色: JavaScript 执行(Scripting)
  • 蓝色: 网络请求(Network)
  • 灰色: 空闲时间(Idle)

我一般先看帧率概览。如果那条绿线经常掉到 30fps 以下,说明有问题。然后点进去看具体哪一帧耗时最长。

关键指标:

  • FPS: 帧率,稳定在 60 最好,低于 30 需要优化
  • CPU: 占用率,超过 80% 说明资源紧张
  • GPU: 内存使用,合成层过多时会飙升

分析帧的构成

点开某一帧,你会看到它被拆成几个阶段。每个阶段都有耗时。我习惯先看 Layout 和 Paint 的时间。如果 Layout 超过 10ms,说明 DOM 结构太复杂或者布局触发太频繁。

举个例子:你给一个元素同时改了 width 和 height,浏览器就得重新计算布局。如果这个元素在文档流里,还会影响周围元素。这就是所谓的「布局抖动」(Layout Thrashing)。

我曾经在一个表格页面里,每 100ms 更新一次数据,每次更新都改了所有单元格的宽高。结果 Layout 耗时从 2ms 涨到 40ms。后来改成只更新内容,不改变尺寸,Layout 时间直接降到 1ms。

避坑指南:用 transform 和 opacity 做动画。这两个属性不会触发 Layout 和 Paint,只走合成阶段。GPU 处理起来非常快。

用 Summary 面板定位瓶颈

Performance 面板底部有个 Summary 标签。它会告诉你这段时间里,各个阶段分别花了多少时间。比如 Scripting 占了 60%,那你就该去优化 JS 代码。如果 Painting 占了 50%,那就考虑减少绘制区域或者用合成层。

我一般这么用:

  1. 先看 Summary,找到耗时最长的阶段
  2. 再点进具体帧,看是哪个操作引起的
  3. 最后看 Call Tree,找到具体的函数调用

三步下来,瓶颈基本就锁定了。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以对照着看,理解 CPU、GPU、主线程和 DevTools 之间的关系。

动画性能瓶颈分析框架 动画性能瓶颈分析框架 性能瓶颈 CPU 负载过高 GPU 负载过高 JS 执行过长 样式计算频繁 布局抖动 合成层过多 纹理内存溢出 绘制区域过大 主线程阻塞 → 帧丢失 DevTools 性能录制与分析 FPS 概览 帧构成分析 Summary 定位

这张图从左到右,从上到下,把性能问题的源头、影响和排查工具串在了一起。你可以把它当成一个检查清单:遇到卡顿,先看 CPU 还是 GPU,再看主线程有没有阻塞,最后用 DevTools 定位具体原因。

嗯,这一章的内容就到这。工具和方法都给了,剩下的就是多练。你动手录几次性能数据,慢慢就能看出门道来。