4、CSS动画与JS动画的抉择:何时用CSS transition/animation,何时用JS控制,GPU加速的触发条件
做前端动画,最常被问到的问题就是:「这个动画用 CSS 还是 JS 写?」
说实话,没有标准答案。但我做了这么多年性能优化,总结了一套自己的判断逻辑。今天就把这套逻辑拆开给你看。
核心原则:能用 CSS 搞定的简单过渡,别碰 JS;需要复杂交互或时间线控制的,别硬用 CSS。
4.1 先看一张决策图
下面这张图,是我自己项目里一直贴着的决策流程。每次不确定用哪种方案,就扫一眼。
4.2 CSS 动画:什么时候用?
我个人习惯是:凡是能用 CSS 解决的简单过渡,绝不写一行 JS。为什么?因为 CSS 动画跑在合成线程上,不阻塞主线程。
4.2.1 适合 CSS 的场景
- 状态切换:hover、active、focus 等伪类触发
- 入场/出场:元素显示隐藏、淡入淡出
- 持续循环:loading 旋转、呼吸灯、脉冲效果
- 单次过渡:页面滚动到某个位置后的元素位移
我的经验:之前做一个后台管理系统,所有按钮的 hover 效果都用 CSS transition 实现。整个项目 200+ 按钮,零 JS 开销。后来接手的人说「这代码真干净」。
4.2.2 CSS 动画的坑
嗯,这里要注意。CSS 动画不是万能的。我踩过几个坑:
- 时间线控制弱:无法暂停、反向、跳转到指定帧
- 复杂缓动函数:cubic-bezier 写不出物理弹跳效果
- 回调机制差:animationend 事件在低版本浏览器有兼容问题
避坑指南:我曾经在一个 H5 活动页里用 CSS animation 做弹窗动画,结果用户快速点击时动画状态错乱。因为 CSS 动画不支持「立即停止并反向播放」。最后改成 JS 控制才解决。
4.3 JS 动画:什么时候用?
说白了,当 CSS 搞不定的时候,就该 JS 上场了。
4.3.1 适合 JS 的场景
| 场景 | 原因 | 推荐方案 |
|---|---|---|
| 复杂缓动曲线 | 物理弹跳、弹簧效果、贝塞尔曲线无法表达 | GSAP / Popmotion |
| 时间线编排 | 多个动画按顺序/并行执行,需要精确控制 | GSAP Timeline / Anime.js |
| 交互驱动 | 拖拽、滚动进度、鼠标跟随 | requestAnimationFrame |
| 动态数据 | 动画参数由后端返回或用户输入决定 | Web Animations API |
关键点:JS 动画跑在主线程上,如果处理不好,很容易造成卡顿。所以能用 requestAnimationFrame 就别用 setInterval,能批量更新就别逐帧操作 DOM。
4.4 GPU 加速的触发条件
这是性能优化的核心。你想想看,为什么有些动画丝般顺滑,有些却掉帧卡顿?
答案就在 GPU 加速上。
4.4.1 什么是 GPU 加速?
浏览器渲染一个页面,通常经过:JavaScript → Style → Layout → Paint → Composite 五个阶段。
如果动画只触发 Composite(合成),不触发 Layout 和 Paint,那就能利用 GPU 的并行计算能力。这就是 GPU 加速。
4.4.2 哪些属性能触发 GPU 加速?
| 属性 | 触发阶段 | 是否推荐 | 说明 |
|---|---|---|---|
transform |
Composite | ✅ 强烈推荐 | translate/scale/rotate 都不触发 Layout |
opacity |
Composite | ✅ 强烈推荐 | 透明度变化,GPU 直接处理 |
will-change |
Composite | ✅ 推荐 | 提前告知浏览器,但别滥用 |
top/left |
Layout + Paint + Composite | ❌ 不推荐 | 每次变化都重新布局,性能差 |
width/height |
Layout + Paint + Composite | ❌ 不推荐 | 同样触发重排 |
我的习惯:做位移动画时,永远用 transform: translateX() 代替 left。哪怕只是移动 1px,也坚持用 transform。这个习惯帮我避免了很多隐性的性能问题。
4.4.3 如何强制开启 GPU 加速?
有时候浏览器不会自动把元素提升到合成层。我们可以手动触发:
/* 方法一:使用 will-change */
.element {
will-change: transform, opacity;
}
/* 方法二:使用 3D 变换(hack 方式) */
.element {
transform: translateZ(0);
/* 或者 */
transform: translate3d(0, 0, 0);
}
/* 方法三:设置固定 backface-visibility */
.element {
backface-visibility: hidden;
}
注意:will-change 不要滥用。我见过有人给页面所有元素都加上 will-change: transform,结果内存暴涨,反而更卡。只在动画开始前加上,动画结束后移除。
4.5 实战对比:一个弹窗动画
拿最常见的弹窗动画举例。两种实现方式,性能差距有多大?
4.5.1 CSS 实现(推荐)
.modal {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: scale(0.8);
opacity: 0;
}
.modal.active {
transform: scale(1);
opacity: 1;
}
4.5.2 JS 实现(不推荐)
// 用 left/top 做位移
modal.style.left = '50%';
modal.style.top = '50%';
// 每次修改都触发 Layout
你想想看,CSS 版本只触发 Composite,JS 版本每次都要 Layout + Paint。60fps 的刷新率下,差距就出来了。
结论:弹窗动画这种「从 A 状态到 B 状态」的简单过渡,CSS 是首选。但如果弹窗需要「从屏幕外弹入,然后抖动两下再停下」,那就得上 GSAP 了。
4.6 我的选择清单
最后,分享一份我平时做决策的清单:
- 先问自己:这个动画是「状态变化」还是「连续运动」?状态变化用 CSS,连续运动用 JS。
- 再看属性:只用 transform 和 opacity?用 CSS。涉及 width/height/color?考虑 JS 或重构设计。
- 考虑交互:用户拖拽、滚动驱动?必须 JS。自动播放的循环动画?CSS 更省资源。
- 检查兼容:需要支持 IE11?CSS animation 部分属性不支持,JS 方案更稳妥。
- 最后测试:打开 DevTools 的 Performance 面板,看有没有 Layout 或 Paint 的耗时操作。
记住一句话:CSS 动画是「声明式」的,JS 动画是「命令式」的。能用声明式解决的问题,就别写命令式的代码。这不仅是性能考虑,也是代码可维护性的考量。