直接改 transform 仍可能掉帧,因浏览器仅对进入合成层的元素启用 GPU 加速,且 rAF 中读取布局(如 getBoundingClientRect)会触发同步重排,破坏渲染流水线。

为什么直接改 transform 仍可能掉帧
很多人以为只要用了 transform 就自动 GPU 加速、永不掉帧,其实不是。浏览器只对「进入合成层」的元素启用硬件加速,而合成层是否创建,受父容器样式(如 overflow: hidden、filter)、图层数量、甚至 JS 是否触发了 layout thrashing 影响。更常见的是:你在 requestAnimationFrame 里一边调 el.style.transform = ...,一边又读 getBoundingClientRect() 或 offsetTop,这会强制浏览器同步重排,直接打断渲染流水线。
requestAnimationFrame 中只写 transform,不读布局
用 requestAnimationFrame 驱动动画本身没错,但关键在「读写分离」——所有布局读取必须前置,所有样式写入必须后置。
- ❌ 错误写法:在 rAF 回调里先算
el.getBoundingClientRect().top,再设el.style.transform - ✅ 正确做法:动画开始前一次性缓存所需尺寸,rAF 里只做纯写操作
- 如果必须动态计算位移(比如跟随手指滑动),用
touchmove的event.touches[0].clientX直接算,别查 DOM - 避免在 rAF 里调
getComputedStyle(el)—— 它是同步 layout 触发器,哪怕你只是想读opacity
配合 CSS @keyframes 更稳,而不是纯 JS 改 style
频繁设置 el.style.transform 会触发 style dirty flag,浏览器需反复比对变更,尤其在长列表或高频动画中,JS 调度开销可能压过渲染收益。用预定义 class + @keyframes 是更轻量、更可控的选择。
- 定义动画:
@keyframes slideIn { from { transform: translateX(-100%); } to { transform: translateX(0); } } - 用
el.classList.add('animate-slide')触发,CSS 内控制animation: slideIn 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94) - 需要 JS 控制进度?用
el.animate(...)API(支持 Chrome 84+、Safari 16.4+),它走合成线程,不进 JS 主线程 - 若兼容老版本,宁可用
transition+ class 切换,也别在 rAF 里狂 set style
will-change 必须动态加,且只加给当前动画元素
will-change: transform 不是性能开关,是内存预告。静态写死在 CSS 里,等于让每个元素都常驻合成层,低端安卓机容易 OOM;不加,又可能错过首帧优化。折中方案是「按需升层」。
立即学习“前端免费学习笔记(深入)”;
- 在
touchstart或mouseenter时,用 JS 设置el.style.willChange = 'transform' - 监听
animationend或transitionend后立刻清空:el.style.willChange = 'auto' - iOS Safari 对短于 16ms 的动画可能不触发
animationend,加兜底:setTimeout(() => el.style.willChange = 'auto', duration + 100) - 绝对不要给列表项批量加,比如
.list-item { will-change: transform; }—— 这是移动端卡顿高发原因
offsetHeight,整条动画流水线就退回 CPU 渲染。



















