CSS 与 JavaScript:动画性能该怎么选
- 原文链接:www.joshwcomeau.com/animation/c...
- 原文作者:Josh W. Comeau

动画性能领域最常见的问题之一,是:基于 JS 的动画是否比 CSS 动画更慢?我们是否应始终优先使用 CSS 过渡,还是可以用 JavaScript 动画库?
这个问题其实比表面看起来微妙得多,我认为一些「常识」并不完全准确。本文会深入探讨,并亲自对比差异。
目标读者(原文 Intended audience 小标题)
本教程中的部分代码片段默认你已熟悉常见的 CSS/JS 动画写法。不过,无论经验深浅,正文的主要结论都应清晰可读。
[对比 CSS 关键帧与 JavaScript 循环](#对比 CSS 关键帧与 JavaScript 循环 "#comparing-css-keyframes-to-javascript-loops-1")
假设我们要实现如下弹跳球动画(原文示例):
下为自原站导出的弹跳球演示 GIF(完整可交互版见 原文):

我们可用 CSS 关键帧实现,具体写法如下:
css
@keyframes bounce {
to {
transform: translateX(calc(var(--bounce-magnitude) * -1));
}
}
.ball {
--bounce-magnitude: 200px;
animation: bounce 1000ms infinite alternate;
}
(这里用 CSS transform 做动画,是因为它能产生最顺滑的运动。若容器尺寸是动态的,就需要在 JS 里计算并设置 --bounce-magnitude。)
或者,我们也可以用 JavaScript 实现!在考虑 GSAP、Motion 这类 JS 库之前,先看纯 JS 版本:
javascript
const startTime = performance.now();
const ball = document.querySelector('.ball');
function animate() {
const elapsedTime = performance.now() - startTime;
// ✂️ 根据已过去的时间计算 `x`。
ball.style.transform = `translateX(${x}px)`;
window.requestAnimationFrame(animate);
}
这段代码用 requestAnimationFrame 在每一帧(多数显示器上约每秒 60 次)调用 animate。计算 x 的核心逻辑因略复杂且与主题无关已省略,若好奇可查看完整代码。
问题来了: 你觉得哪种方式跑起来更流畅?
多数人直觉会认为 CSS 版性能更好------直觉没错,但原因可能和你想的不一样。😅
你可能以为 JS 版更慢,是因为每帧都要额外计算 x,或是因为 JavaScript 与 DOM 之间「跨桥」有额外开销。但现代浏览器引擎处理这些轻而易举;即便在低端设备上,也只需极短的一小段时间,远不足以影响动画帧率。
但有一个关键差异: JavaScript 版跑在主线程 上,与应用里其他一切共享。CSS 过渡与关键帧动画跑在独立线程上,因此 JavaScript 里发生阻塞时,它们不会被打断。
我做了个模拟演示。 点击「Play」按钮运行演示。每隔几秒,主线程会被故意阻塞。观察它对下面两种动画的影响:
下为自原站导出的演示 GIF(约 12 秒,含主线程阻塞周期;完整可交互版见 原文):

在现代 Web 应用里,主线程非常 忙。React 等 JavaScript 框架不断更新 DOM,以与应用状态同步。每次 fetch(例如加载更多数据或刷新现有数据),响应也要由主线程解析。
所以,若你见过加载指示器在 UI 更新前卡一下,原因就在这里!基于 JavaScript 的动画必须与应用其余部分争抢算力。
另一个细节(An additional wrinkle,原文小标题)
除了比较 JS 与 CSS,我们还要考虑正在动画化的 CSS 属性 。本页演示使用 transform,它在变化时不会触发任何布局或绘制重算,但大多数 CSS 属性并非如此。
若改用 margin-left 而非 transform,每次变化、每一帧都会使布局失效。布局重算发生在主线程上,因此这类工作会堵在路上,等待 JavaScript 工作完成。所以在动画场景里,坚持使用 transform、opacity、filter 等合成器友好(compositor-friendly)属性,收益很大。
感谢 Bramus 的提醒!他有一篇很棒的博文,深入探讨了与自定义属性动画相关的类似陷阱。
对比动画库
上面的例子里,我用 requestAnimationFrame 循环在 JavaScript 中逐帧更新 UI。这是相当底层的写法;实践中,很多开发者会用提供更高级抽象的 JavaScript 动画库。
我们来对比两个流行库:Motion(原 Framer Motion)与 GSAP:
下为自原站导出的演示 GIF(CSS 关键帧 / Motion / GSAP 三路对比;完整可交互版见 原文):

有意思!Motion 和 GSAP 都是 JavaScript 库,你可能预期它们都会受主线程限制。但不知为何,主线程繁忙时 Motion 仍能保持动画流畅。🤔
秘诀在于 Motion 底层使用 Web Animations API(WAAPI)。WAAPI 本质上是接入与 CSS 关键帧动画相同底层引擎的 JavaScript 接口。因此 Motion 能把动画放到独立线程上,避开多数其他 JavaScript 动画库的主要陷阱!😮
公平地说,GSAP 功能极其强大,包含许多可能与 WAAPI 不兼容的能力。所以不是 GSAP 选错了,而是两者做了不同的权衡。
不同步(Out of sync,原文小标题)
这里还有一处值得留意的细节差异。
主线程变忙时,我的 requestAnimationFrame 实现会冻结,但随后会瞬间跳到正确位置,与 CSS 关键帧动画保持同步。
GSAP 版则不会保持同步。主线程恢复空闲后,GSAP 动画从当前位置继续:
下为自原站导出的同步行为对比 GIF(完整可交互版见 原文):

注意到 GSAP 小球如何与其他两个不同步了吗?
这是因为 GSAP 不追踪动画开始以来经过的时间,而是专注于每帧移动相同距离------即便那些帧被延迟了。
一般而言, 我们希望动画保持同步。若动画应耗时 500ms,无论播放是否流畅,500ms 后都应结束!我常编排多个动画按特定顺序运行,若动画时长不可靠,编排就难以为继。
我们某种程度上可以 用 animationend / transitionend 事件处理器绕过对固定时长的依赖,但这仅适用于完全顺序的动画。实践中,我往往希望动画略有重叠、形成错落而非严格一个接一个。
不过,也可以说 GSAP 的做法在特定场景下体验更好------丢帧后元素不会立刻瞬间跳到新位置。因此究竟优化什么,取决于你的目标。
预先下载成本(Upfront download costs,原文小标题)
我还没讨论的一点是:JavaScript 动画库在使用前需要下载并解析。这些库往往不小;Motion gzip 后约 48kB,GSAP gzip 后约 27kB(实际往往更大,因为 GSAP 许多有价值功能分散在可选插件里)。
我的「争议观点」是:多数情况下这并不算大问题 😅。即便在慢网、低端设备上,加载动画库通常也只需一两秒;因此只有当你需要在用户进入页面的最初几秒内就要做动画时,才可能影响体验。
我通常不想 在页面加载后立刻 做动画;除了淡入这类简单效果(不需要 JS 库),我的动画多由用户交互触发,而用户一般不会那么快就开始操作页面内容。
我能想到的一个例外是滚动驱动动画------用户确实 会很快开始滚动!但如今可用 Animation Timeline API 处理滚动动画,无需库。
选对工具
我自己的做法是:能原生 CSS 动画/过渡就用。CSS 单独搞不定时,我会选 Motion 这类库------在解决复杂需求的同时,避开 JS 库常见的弊端。
不过 CSS 已强大到,如今真正需要动画库的场景并不 多;View Transitions、linear() 与 Animation Timeline 等新 API,让你无需 JavaScript 也能做很多事。✨
我在全新课程 Whimsical Animations 里深入讲解这些工具及更多内容,展示如何用现代 CSS、JavaScript、SVG 与 Canvas 设计与实现顶级动画。

如今大语言模型很擅长生成语法,但我们仍需要自己的判断。本文讨论了 CSS 与 JavaScript 的性能考量,我的课程里充满这类内容。因此无论你手写代码与否,这门课都能帮你做出出色的动画与交互。
你可以在下列链接了解更多课程信息:
两类动画库(Two kinds of animation libraries,原文小标题)
最后一点建议:市面上的动画库大致分两类:
-
扩展可创建动画范围的库。
-
对 CSS 内置能力(如过渡与关键帧动画)做 JavaScript 封装的库。
Motion 与 GSAP 都属于第一类------它们打开新可能,例如在不同 SVG 形状之间变形过渡,现代 CSS 尚做不到。
*具体就 path 元素而言,我们其实可以用 CSS 过渡在 path 定义之间做动画!但该能力截至 2026 年 5 月尚未在 Safari 中可用。
然而,我见过的大多数库属于第二类。它们并不提供新功能,只是包装 CSS 的过渡等特性,让你用 JavaScript 而非 CSS 写基础动画。
我的真实看法是:第二类工具不值得用。它们带来本文所述的「主线程」负担,还会膨胀 JavaScript 包体积,却几乎没什么回报。基础过渡不需要 JS 接口------CSS 已经非常好用了!
因此评估动画库时,我会问:它能让我做哪些新颖的事?若没有好答案,就不用它。