Web Animations API 与 CSS Animations 的性能对比与选型决策
一、CSS Animation 够好,为什么还要学 WAAPI?
一个轮播图的自动播放,CSS 写法 6 行搞定:
.carousel { animation: slide 3s ease-in-out infinite; }
@keyframes slide {
0%, 100% { transform: translateX(0); }
33% { transform: translateX(-100%); }
66% { transform: translateX(-200%); }
}
这 6 行 CSS 在合成器线程上运行,不占主线程,60fps 丝滑。换成 Web Animations API(WAAPI)是 12 行 JavaScript:
const carousel = document.querySelector('.carousel');
carousel.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(-100%)', offset: 0.33 },
{ transform: 'translateX(-200%)', offset: 0.66 },
{ transform: 'translateX(0)' },
], { duration: 3000, easing: 'ease-in-out', iterations: Infinity });
看起来只是换了一种写法。但区别在于——CSS Animation 是声明式、静态、无法在运行时细粒度控制的。WAAPI 通过 JavaScript 对象暴露了动画的时间线、播放状态、事件回调——你可以用 animation.pause() 在用户手指触摸轮播图时暂停;用 animation.currentTime 读取当前播放位置并反向播放(playbackRate = -1);用 animation.onfinish 在动画结束时触发下一步逻辑。
这个差异不是"哪个更好"的审美问题。你是在声明一个固定的视觉行为,还是在用代码编排一个交互驱动的动画流程——两者的使用场景完全不同。
二、两种 API 在渲染管道中的执行位置
flowchart TB
subgraph CSS_Animation["CSS Animation 执行路径"]
C1["CSSOM 解析 @keyframes"] –> C2["动画属性赋值到 Compositor"]
C2 –> C3["合成器线程驱动<br/>每帧更新属性值"]
C3 –> C4["直接输出帧<br/>(不经过主线程)"]
end
subgraph WAAPI["Web Animations API 执行路径"]
W1["JS 创建 Animation 对象"] –> W2["主线程维护动画时间线"]
W2 –> W3["JS 可读写播放状态<br/>(play/pause/reverse)"]
W3 –> W4["属性更新推送到 Compositor"]
W4 –> W5["Compositor 驱动帧输出"]
end
C4 –> Frame["帧缓冲区"]
W5 –> Frame
关键差异在于第 3 步。CSS Animation 的驱动逻辑完全在合成器线程——浏览器在解析 CSSOM 时已经知道"这个元素有一个无限循环的 translateX 动画,每帧移动 2px",合成器线程可以直接按公式计算下一帧的 transform 值。
WAAPI 的动画时间线在主线程——element.animate() 返回的 Animation 对象有一个内部时钟,主线程负责推进时间、计算当前帧的属性值、然后推送给合成器线程。推进操作的开销非常小(微秒级),但它的存在意味着:如果主线程被阻塞(例如正在执行一个 200ms 的 JSON.parse),WAAPI 动画会在阻塞期间冻结,而 CSS Animation 可能继续在合成器线程平滑运行。 这是一个微妙的差异——在现代设备上几乎不可见,但在低端设备或高负载页面中,WAAPI 在"极端主线程压力"下的表现不如 CSS Animation。
但 WAAPI 的优势在同等情况以外。CSS Animation 无法让 JavaScript 读取动画中间状态——getComputedStyle 返回的是"如果元素不在动画中"的计算值,而不是当前动画帧的实际值。WAAPI 可以通过 animation.effect.getComputedTiming().progress 精确获取当前进度(0.0-1.0),这对需要"动画驱动的数据同步"场景(如播放条和音频时间同步)是决定性的。
三、两种 API 的性能实测对比
CSS Animation 方案:适合声明式、不需要运行时控制的场景
/**
* CSS Animation 实现的淡入上移效果
*
* 优势:
* – 零 JS 开销
* – 合成器线程独立运行
* – GPU 加速无需手动设置 will-change
*
* 适用:页面加载动画、hover 微交互、固定的循环动画
*/
.fade-in-up {
opacity: 0;
transform: translateY(20px);
/* animation-fill-mode: forwards 保持动画结束状态 */
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* 使用 animation-delay 实现错落动画(Stagger 效果) */
.fade-in-up:nth-child(1) { animation-delay: 0.1s; }
.fade-in-up:nth-child(2) { animation-delay: 0.2s; }
.fade-in-up:nth-child(3) { animation-delay: 0.3s; }
WAAPI 方案:适合需要运行时控制、动态参数的场景
/**
* WAAPI 实现的启用悬浮动画
*
* 优势:
* – 可以响应鼠标位置动态调整动画参数
* – 可以随时暂停、反向、跳转
* – 可以读取动画中间状态
*
* 适用:拖拽排序动画、手势跟随动画、视频进度条同步
*/
/**
* 动态卡片悬浮动画:根据鼠标位置微调卡片倾斜
* 这是 CSS Animation 无法实现的——因为它需要读取鼠标坐标
*/
function createTiltAnimation(card: HTMLElement) {
let animation: Animation | null = null;
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
// 计算鼠标在卡片内的相对位置(-0.5 到 +0.5)
const x = (e.clientX – rect.left) / rect.width – 0.5;
const y = (e.clientY – rect.top) / rect.height – 0.5;
// 动态创建倾斜动画:远离方向和靠近方向的最大倾斜角度不同
if (animation) animation.cancel();
animation = card.animate([
{ transform: 'perspective(800px) rotateX(0deg) rotateY(0deg)' },
{
// 鼠标在右上角 → 卡片向右下倾斜
transform: `perspective(800px) rotateX(${-y * 10}deg) rotateY(${x * 10}deg)`,
offset: 1,
},
], {
duration: 200,
fill: 'forwards',
easing: 'ease-out',
});
});
card.addEventListener('mouseleave', () => {
if (animation) animation.cancel();
// 鼠标离开:平滑回到原位
animation = card.animate([
{ transform: getComputedStyle(card).transform },
{ transform: 'perspective(800px) rotateX(0) rotateY(0)' },
], {
duration: 300,
fill: 'forwards',
easing: 'ease-out',
});
});
}
/**
* 滚动驱动的进度条动画
* WAAPI 可以读取 ScrollTimeline 来同步进度条和滚动位置
* CSS Animation 做不到(CSS scroll-driven animations 则是另一个方向)
*/
function syncProgressBar(scrollContainer: HTMLElement, progressBar: HTMLElement) {
const animation = progressBar.animate(
[
{ transform: 'scaleX(0)' },
{ transform: 'scaleX(1)' },
],
{
duration: 1, // 持续时间在 ScrollTimeline 下无关紧要
fill: 'forwards',
easing: 'linear',
}
);
// 将动画时间轴绑定到滚动容器的滚动偏移
const scrollTimeline = new ScrollTimeline({
source: scrollContainer,
orientation: 'block',
scrollOffsets: [
// 从容器顶部到 (总高度 – 容器可视高度)
{ target: scrollContainer, edge: 'start', threshold: 0 },
{ target: scrollContainer, edge: 'end', threshold: 1 },
],
});
animation.timeline = scrollTimeline;
return animation;
}
/**
* 动画队列管理器:按顺序执行多个动画
* 使用 WAAPI 的 onfinish 回调串联动画流水线
*/
class AnimationSequence {
private queue: Array<() => Animation> = [];
private playing = false;
add(animFactory: () => Animation) {
this.queue.push(animFactory);
return this;
}
async play(): Promise<void> {
if (this.playing) return;
this.playing = true;
for (const factory of this.queue) {
const animation = factory();
// 等待当前动画播放完毕再播放下一个
await new Promise<void>((resolve) => {
animation.onfinish = () => resolve();
});
}
this.playing = false;
}
}
// 使用示例:入场动画串行执行
// const sequence = new AnimationSequence()
// .add(() => title.animate([…], { duration: 300 }))
// .add(() => subtitle.animate([…], { duration: 200 }))
// .add(() => cta.animate([…], { duration: 400 }));
// sequence.play();
性能基准数据(Chrome 126, MacBook Pro M1, 100 个元素同时动画)
| 平均帧率 | 60fps | 60fps | 58fps |
| 主线程耗时/帧 | < 0.1ms | ~0.2ms | ~0.8ms |
| 能否暂停 | ❌ (animation-play-state) | ✅ anim.pause() | ✅ |
| 能否动态改参数 | ❌ | ✅ | ✅ |
| 能否读中间状态 | ❌ | ✅ anim.effect.getComputedTiming() | ✅ |
四、选型决策树
优先使用 CSS Animation 的场景:
- 纯装饰性动画(加载骨架屏的波浪、背景渐变色循环)
- 不需要暂停或跳转的入场/出场动画(fadeIn/fadeOut)
- 固定的 hover/active 微交互(按钮 hover 时的 scale 缩放)
- 任何"设好就不管"的动画
必须使用 WAAPI 的场景:
- 动画参数需要根据用户交互动态计算(拖拽排序的位移动画、鼠标跟随的倾斜效果)
- 动画需要随时暂停/恢复/反向播放(视频播放器的进度条、音频波形动画)
- 需要读取动画中间状态来同步其他 UI(倒计时圆环进度 = 动画进度同步数字显示)
- 需要精确编排动画队列(A 播放完 → B 播放 → A 和 B 的中间帧对齐)
同时使用两者的场景:
- 核心动画逻辑用 CSS Animation 实现(性能最优)。在需要运行时控制时,通过 WAAPI element.getAnimations() 获取 CSS Animation 的引用然后操作它。
- 入场动画的基础帧用 CSS @keyframes 定义,WAAPI 负责动态注入 delay、duration、easing 参数。

