上周接手了一个"老项目"——运营反馈说首页轮播"卡得像 PPT",打开 Chrome DevTools 一看,帧率稳定在 12fps。
经过两小时排查,最终只改了不到 20 行代码,帧率拉满到 60fps。
这篇文章记录整个过程,顺手把浏览器渲染原理一起讲清楚。
问题复现
原始代码长这样:
.card { transition: all 0.3s ease;}.card:hover { top: -8px; width: 105%; box-shadow: 0 20px 40px rgba(0,0,0,0.2);}setInterval(() => { card.style.left = `${position}px`; position -= 2;}, 16);看起来很普通对吧?但这里藏着两个致命问题。
浏览器渲染流水线
要理解为什么卡,先要知道浏览器怎么"画"页面。
每一帧的渲染,浏览器要走这条流水线:
JavaScript → Style → Layout → Paint → Composite阶段 | 干了什么 | 触发代价 |
Layout(回流)
| 重新计算元素位置、尺寸 | ⚠️ 极高 |
Paint(重绘) | 重新绘制像素 | 较高 |
Composite(合成) | GPU 合成图层 | ✅ 极低 |
关键结论只有一条:
动画只要触发 Layout,必卡无疑。
找出罪魁祸首
打开 Chrome DevTools → Performance 面板,录制 3 秒动画:
录制结果(优化前):
- 大量紫色 Recalculate Style 条
- 大量黄色 Layout 条
- 每帧耗时 83ms(正常应 ≤16ms)
问题清晰了:
- top 属性变化 → 触发 Layout
- width 属性变化 → 触发 Layout
- left 属性变化 → 触发 Layout
- transition: all → 把所有属性变化都纳入动画,包括那些会触发 Layout 的
每帧都在反复触发全页面重排,12fps 完全合理。
优化方案
方案一:用 transform 替代位移属性
/* ❌ 触发 Layout */.card:hover { top: -8px; width: 105%;}/* ✅ 只触发 Composite */.card:hover { transform: translateY(-8px) scale(1.05);}原理:transform 和 opacity 是浏览器专门优化过的属性,变化时完全绕过 Layout 和 Paint,直接由 GPU 在合成阶段处理。
// ❌ 修改 left,每次触发 LayoutsetInterval(() => { card.style.left = `${position}px`;}, 16);// ✅ 修改 transform,只触发 CompositesetInterval(() => { card.style.transform = `translateX(${position}px)`;}, 16);方案三:限制 transition 范围
/* ❌ 监听所有属性变化 */transition: all 0.3s ease;/* ✅ 只监听安全属性 */transition: transform 0.3s ease, opacity 0.3s ease, box-shadow 0.3s ease;.card { will-change: transform;}这行代码告诉浏览器:“这个元素即将发生 transform 变化,请提前为它创建独立图层。”
浏览器会把该元素提升到独立的 GPU 图层,动画时完全不影响其他元素。
⚠️ 注意:will-change 不要滥用。每个独立图层都消耗显存,页面上所有元素都加这个属性,显存会爆。只在频繁动画的元素上使用。
最终代码
.card { will-change: transform; /* 只对安全属性开启过渡 */ transition: transform 0.3s ease, box-shadow 0.3s ease;}.card:hover { /* 用 transform 替代 top/width */ transform: translateY(-8px) scale(1.05); box-shadow: 0 20px 40px rgba(0,0,0,0.2);}// 用 requestAnimationFrame 替代 setIntervallet position = 0;function animate() { position -= 2; card.style.transform = `translateX(${position}px)`; requestAnimationFrame(animate);}requestAnimationFrame(animate);requestAnimationFrame 相比 setInterval 的优势:
- 自动与屏幕刷新率同步(60Hz 屏幕 = 每 16.6ms 执行一次)
- 页面不可见时自动暂停,节省性能
- 不会有 setInterval 的时间漂移问题
优化后结果
再次录制 Performance:
- Layout 条:消失
- 每帧耗时:从 83ms → 4ms
- 帧率:稳定 60fps
速查表:哪些属性安全,哪些危险?
属性 | 触发阶段 | 能否用于动画 |
transform | Composite | ✅ 随便用 |
opacity | Composite | ✅ 随便用 |
color 、background-color | Paint | 少量可用 |
top 、left、margin | Layout | ❌ 避免动画 |
width 、height、padding | Layout | ❌ 避免动画 |
font-size 、line-height | Layout | ❌ 避免动画 |
记住一句话:动画只用 transform 和 opacity,其他属性用静态设置。
前端性能优化看着玄,其实大多数动画卡顿问题都归结于同一个根源——不必要地触发了 Layout。
搞懂这一条,80% 的动画性能问题就解决了。
