欢迎光临
我们一直在努力

Web Animations API 与 CSS Animations 的性能对比与选型决策

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 个元素同时动画)

指标CSS AnimationWAAPI (无读取)WAAPI (每帧读取 progress)
平均帧率 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 参数。

五、总结

  • CSS Animation 在合成器线程运行,WAAPI 通过主线程驱动——主线程阻塞时 CSS Animation 可能继续平滑运行。
  • WAAPI 的核心价值不是"另一种写动画的方式",而是"动画对象化"——可以暂停、读取、编排、动态传参。
  • element.animate() 返回的 Animation 对象等同于用 JS 创建了一个同 CSS @keyframes 等价的动画,但可编程控制。
  • animation.effect.getComputedTiming().progress 可以读取当前动画进度(0-1),CSS Animation 无等效操作。
  • WAAPI 的 onfinish 回调天然适合动画队列编排——"A 播放完 → B 播放"的流水线。
  • ScrollTimeline + WAAPI 可以实现滚动驱动的动画,比手动 rAF 同步更高效。
  • CSS Animation 的 animation-play-state: paused 可以模拟暂停但不能在暂停状态下修改动画参数。
  • WAAPI 的所有优势在"只播放不动它"的场景中都是零价值——此时 CSS Animation 更简洁且性能稍优。
  • 性能差异在 90% 的场景中可以忽略——选型依据是"是否需要运行时控制",不是"哪个更快"。
  • 声明式场景选 CSS Animation,交互式场景选 WAAPI,Mix 场景用 CSS @keyframes + WAAPI 控制参数。
  • 赞(0)
    未经允许不得转载:171主机测评 » Web Animations API 与 CSS Animations 的性能对比与选型决策
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址