如果你试图在项目里通过 CSS 变量去动态修改一个动画的持续时间,或者想在进程过半时暂停动画,再从 35% 的位置重新启动,大概很快就会撞上 CSS Animation 的限制。这些操作并不是做不到,但往往意味着你要额外维护一堆状态变量,甚至把动画拆成多段来模拟。

Web Animations API(WAAPI)的出现,就是为了把这些控制能力收编进 JavaScript 的运行时模型里。它不是一个要替代 CSS Animation 的新框架,而是提供了一套更底层的动画控制机制。这篇文章会从实际使用出发,看一下 WAAPI 的核心设计、常见误区和工程落地建议。
为什么 CSS Animation 会先撞到天花板
CSS Animation 的声明式模型非常适合“静态”的动画效果——你确定好从A到B,定义好持续时间和缓动函数,然后交给浏览器渲染。问题出现在交互上。当动画需要响应某个异步状态,比如网络请求返回、用户拖拽进度条、或者设备方向变化时,你往往会发现缺少一个直接控制动画内部进程的接口。
animation-play-state 可以暂停和继续,animation-delay 可以延后开始,但这些能力的粒度太粗。你想知道动画当前跑到哪个关键帧,想修改某段中间态,想同时控制多个动画的同步关系,这些在 CSS Animation 中都需要变通。
WAAPI 的核心概念:KeyframeEffect 与 Animation
WAAPI 把动画拆成了两个独立的部分。KeyframeEffect 描述动画的视觉表现,包括从哪里到哪里、每个关键帧的具体属性、时间函数;Animation 则是控制动画生命周期的播放器对象,它持有 currentTime、playbackRate、playState 这些运行时状态。
这种拆分带来的直接好处是:同一个 KeyframeEffect 可以被多个 Animation 复用,不同的 Animation 又可以利用同一个 document.timeline,保证多个元素之间的时间基准一致。CSS Animation 做不到这一点,它只会被动地根据样式表中的定义执行。
const flying = target.animate(
[
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(200px)', opacity: 0 }
],
{
duration: 800,
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
fill: 'forwards'
}
);
// 用户点击暂停
flying.pause();
// 稍后从 60% 位置继续
flying.currentTime = flying.effect.getComputedTiming().duration * 0.6;
flying.play();
这段代码展示了 WAAPI 的核心能力:动画对象不是一个扑通发射出去就脱管的东西,而是可以暂停、跳转进度、重新开始运行的可控对象。即便动画已经结束,你也可以把 currentTime 拨回去重新播放,这种细腻的操控在 CSS Animation 里几乎不可能干净地实现。
与 CSS Animation 的对比
理解两者差异,不能只看 API 风格,还要看它如何影响你的代码结构。下面这张表列出了几个关键维度的差异。
| 对比维度 | CSS Animation | Web Animations API |
|---|---|---|
| 定义位置 | 样式表 / CSS变量 | JavaScript |
| 动态修改参数 | 通过CSS变量间接修改 | 直接修改effect或timing |
| 暂停/继续 | animation-play-state | pause() / play() |
| 播放进度控制 | 不支持 | currentTime直接设置 |
| 多动画同步 | 依赖delay计算 | 共享timeline或逻辑编排 |
| 运行状态观察 | 事件有限 | 可读取playState / currentTime |
| 适用场景 | 简单声明式动画 | 需要程序化控制的复杂交互 |
关于 WAAPI 的几个常见误区
有一种说法认为 WAAPI 只是给 CSS Animation 换了个 JavaScript 外壳。实际上,WAAPI 的设计思路与 CSS 动画完全不同,它更接近一个微型动画引擎。你可以在同一个 KeyframeEffect 上叠加不同的 Animation 实例,也可以把多个动画的播放进度关联到同一个时间轴上,做动画编排时就相当于在写逻辑。
也有人觉得用了 WAAPI 性能就一定更好。这取决于你动画修改的属性。动画性能的关键依然在于是否只合成 transform 和 opacity。如果动画过程修改了 height 或 width,无论 API 多现代,依然可能触发布局。而且通过 requestAnimationFrame 不停修改动画参数时,如果控制不好,反而会让合成器提前优化失效。
还有一个更普遍的倾向,就是试图用 WAAPI 替代所有 CSS 动画。声明式动画在大多数场景下仍然更简单、更可靠。比如悬浮状态、页面加载时的入场动画,直接用 CSS 写在样式表里,可读性和可维护性都更好。WAAPI 最合适的场景是那些需要依赖程序运行时状态来动态调整的动画。
什么情况下应该切换到 WAAPI
举一个实际例子。一个音频文件管理工具里,每条记录都有一段音量变化的波形图,用户点击播放后,波形条要随着当前音量实时伸缩。用 CSS Animation 定义波形动画也可以,但每条记录的初始时间、速度和强度都不同,并且需要根据网络传输状态随时调整。使用 WAAPI 后,波形条的动画速度直接关联到音量值,代码里只需要修改 playbackRate 就能让画面跟随节奏,完全不需要重建动画。
另一个常见场景是轮播图的拖拽切换。我们希望用户在拖拽时动画跟随手指,松开后根据速度完成整页过渡。这类交互的核心在于动画进度必须与手势实时相关,CSS Animation 的 delay 和 play-state 很难做到这一点。而 WAAPI 可以直接把 currentTime 映射到拖拽位移,让动画反馈和手势处于同一时间线。
数据可视化的过渡动画也越来越依赖这种能力。比如折线图的数据范围变化时,旧路径要平滑过渡到新路径,动画时长需要根据数据变化幅度动态计算。WAAPI 允许你在创建动画时直接传入计算出来的 duration,而不需要预先在样式表里写死几个档位。
落地时怎么开始
如果确认你的项目需要 WAAPI,建议从最简单的 element.animate() 开始。这个语法几乎和 CSS Animation 一样,但返回值是 Animation 实例,你可以立刻用它做暂停、继续、跳转等操作。等动画逻辑复杂到需要复用效果时,再重构为 Animation + KeyframeEffect 的组合。
const pulse = element.animate(
[{ transform: 'scale(1)' }, { transform: 'scale(1.2)' }],
{ duration: 600, iterations: Infinity }
);
// 改变运行速度
pulse.playbackRate = 1.5;
有几个细节值得注意。动画结束后,通过 fill: 'both' 可以保留关键帧计算值,但当你需要销毁动画时,记得调用 finish() 或 cancel(),否则匿名 keyframe 对象可能一直留在内存里。另外,prefers-reduced-motion 在 WAAPI 中没有自动生效,你需要手动检测后决定是否播放动画。
经验提醒:不要忘记在动画实例上调用 cancel() 或 finish() 来释放资源。对于频繁创建动画的应用,比如列表筛选时的过渡,这能避免掉大量无用的动画对象堆积在内存里。
如何决定用哪一套方案
判断标准其实很直接。如果动画是静态的、由样式表驱动的,例如 hover 效果、按钮出现动画,继续用 CSS Animation。如果动画需要监听用户输入、需要动态调整持续时间、需要暂停和恢复,或者必须与另一段动画保持严格的时序关系,那就用 WAAPI。
同时也要注意浏览器兼容性。现代浏览器对 WAAPI 的基础支持已经相当好,但 Safari 中对某些方法的实现仍然存在差异,比如 Animation.replaceState 和部分事件。团队项目建议通过 feature detect,使用 if ('animate' in element) 做渐进增强,在不支持的浏览器回退到 CSS Animation。
- 动画需要直接关联用户输入,例如拖拽、滑动、点击进度条。
- 动画的持续时长或关键帧位置需要根据运行时数据实时变化。
- 需要一个独立的控制器暂停、继续、跳转整个动画流程。
- 多个动画对象需要共享同一个时间轴,保证同步播放。
Web Animations API 并不打算取代 CSS Animation,而是把你从样式表的三层皮里解放出来,让动画重新变成一种可编程的行为。它的灵活性不是体现在代码更短,而是体现在你可以用一套统一的时间轴逻辑,去处理那些以前需要多个 hack 才能完成的交互。如果你的项目已经出现了控制动画状态的复杂状态机,也许就是引入 WAAPI 的好时机。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/860/