4、CSS动画与JS动画的抉择:何时用CSS transition/animation,何时用JS控制,GPU加速的触发条件

做前端动画,最常被问到的问题就是:「这个动画用 CSS 还是 JS 写?」

说实话,没有标准答案。但我做了这么多年性能优化,总结了一套自己的判断逻辑。今天就把这套逻辑拆开给你看。

核心原则:能用 CSS 搞定的简单过渡,别碰 JS;需要复杂交互或时间线控制的,别硬用 CSS。

4.1 先看一张决策图

下面这张图,是我自己项目里一直贴着的决策流程。每次不确定用哪种方案,就扫一眼。

CSS动画与JS动画决策流程图 CSS 动画 vs JS 动画 决策流程 动画需求 是简单状态过渡? 是 否 CSS 需要复杂交互? 是 JS 动画 否 触发 GPU 加速? GPU 加速触发条件 transform: translate/scale/rotate opacity 变化 will-change 属性声明 提示:优先使用 transform + opacity 组合,避免触发 layout 和 paint

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 我的选择清单

最后,分享一份我平时做决策的清单:

  1. 先问自己:这个动画是「状态变化」还是「连续运动」?状态变化用 CSS,连续运动用 JS。
  2. 再看属性:只用 transform 和 opacity?用 CSS。涉及 width/height/color?考虑 JS 或重构设计。
  3. 考虑交互:用户拖拽、滚动驱动?必须 JS。自动播放的循环动画?CSS 更省资源。
  4. 检查兼容:需要支持 IE11?CSS animation 部分属性不支持,JS 方案更稳妥。
  5. 最后测试:打开 DevTools 的 Performance 面板,看有没有 Layout 或 Paint 的耗时操作。

记住一句话:CSS 动画是「声明式」的,JS 动画是「命令式」的。能用声明式解决的问题,就别写命令式的代码。这不仅是性能考虑,也是代码可维护性的考量。