配了三年动画性能,每次卡都去改 JS 或盯 Lighthouse——今天发现根子全在 CSS 这三个属性选错了

页面跑起来帧率不稳,Lighthouse 全绿但用户说卡——大多数团队的第一反应是去 JS 里找原因,或抱怨电脑不够好。但 Chrome DevTools 的 Performance 面板每次都在告诉你同一件事:根子在前端写的 CSS 上。

今天把这件事从根上拆开。


浏览器渲染流水线:六个阶段,每个阶段都有代价

浏览器跑一帧要经过六个阶段:

Style → Layout → Paint → Composite

每改变一个 CSS 属性,浏览器不一定全跑一遍。它只跑「必须跑的那些」。关键在于你改的是什么属性:

① Layout(回流):改变 width、height、margin、padding、top、left、font-size——浏览器要重新算整个 DOM 树的几何关系,最贵。

② Paint(重绘):改变 background-color、color、box-shadow——几何没变,但像素变了,要重新画出来,中等开销。

③ Composite(合成):只改 transform、opacity——浏览器只需要把已有的层重新合成,开销最小。

这条规则直接决定了你写的动画是跑在 主线程(被 JS 抢占)还是 合成器线程(独立运行,JS 堵不住它)。


transform + opacity:CSS 动画的性能天花板

来看最直接的对比:

❌ 贵的写法:每次帧都触发 Layout + Paint + Composite

.card {
  transition: left 0.3s ease, width 0.3s ease;
}
.card:hover {
  left: 100px;
  width: 110%;
}

✅ 便宜的写法:只触发 Composite

.card {
  transition: transform 0.3s ease, opacity 0.3s ease;
}
.card:hover {
  transform: translateX(100px) scale(1.1);
  opacity: 0.9;
}

两套代码实现的效果可能一模一样——但后者在主线程被 JS 塞满的时候依然跑满 60fps,前者已经开始跳帧。

为什么?当动画只涉及 transform 和 opacity,浏览器把这个元素的渲染升格到「自己的合成层」,完全绕过主线程。JS 在那边跑业务逻辑,动画在这边自己跑自己的。


content-visibility:折叠线以下的内容,浏览器根本不该渲染

一个长页面首屏加载慢,根子不全在资源大小——是浏览器在认真渲染那些用户还没滑到的内容。

.article-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 800px;
}

加了 content-visibility: auto 的元素:

  • 在视口外时,浏览器跳过它的 Layout + Paint,一个像素都不画
  • 进入视口前,DOM 还在、可搜索、焦点可到达(和 display: none 不一样)
  • 配合 contain-intrinsic-size 预设高度,还可以防止内容突然出现时的滚动条抖动

实测数据(EffiFlow 2026-07 的对比):强制布局从 27.3ms 压到 1.8ms,只加了一行 CSS。

要注意的坑:如果你对屏幕外的元素调用 getBoundingClientRect()、offsetTop 这类读取布局的 API,浏览器会当场把那个元素强制渲染一遍——省下来的全没了。


will-change:提前告诉浏览器「我要动了」,别等跑起来再建层

默认情况下,浏览器在动画开始时才创建合成层,有一帧的延迟。will-change 可以提前预约:

.modal-overlay {
  will-change: transform, opacity;
}

但这条属性的本质是「提示」,不是命令。用过头了,每个合成层都吃 GPU 显存,反而伤性能。

最佳实践是「按需预约,用完即删」:

.modal-overlay {
  will-change: transform, opacity;
}
.modal-overlay.done {
  will-change: auto;
}

下一步:怎么判断自己的动画到底贵不贵

Chrome DevTools 的 Rendering 面板可以实时告诉你答案:

  1. 打开 DevTools → Rendering(三个点 → More tools → Rendering)
  2. 勾选 Paint Flashing——页面被重绘的区域会闪绿
  3. 勾选 Layer Borders——合成层会用蓝边框标出来
  4. 打开 Performance 面板录一段滚动或动画,看紫色(Layout)和绿色(Paint)条有多高

如果动画正在跑的时候紫色条频繁出现,问题在 Layout;如果绿色条频繁出现但紫色没有,问题在 Paint;如果两者都没有但帧率还是低——去看 JS 主线程的 Long Task。

这条判断路径本身就值三年经验。今天免费拿走。


结论:CSS 动画性能的核心不在 JS 写了什么,在 CSS 属性触发了渲染流水线的哪一段。transform/opacity 永远是最优解;content-visibility 把折叠线以下的内容从渲染队列里删掉;will-change 是锦上添花,不是万金油。三条规则,没有一条需要装依赖,浏览器自己会。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 13 阅读