Web AnimationsAPI
浏览器内置的动画引擎,让你用一行 JavaScript 就能驱动高性能交互动效——无需任何第三方库。
💡 一句话理解
如果 CSS animation 是预设菜谱, JavaScript 动画是手动炒菜,那么 WAAPI 就是智能炒菜机—— 既能精准控制每一步,又让底层 GPU 加速帮你"开大火",性能与灵活兼得。
L3 · Core
核心架构与工作原理
WAAPI 的渲染管线
1
KeyframeEffect
定义关键帧数组
& 动画选项
2
Animation
可编程的播放控制器
play/pause/reverse/finish
3
AnimationTimeline
默认 Document 时间线
或 ScrollTimeline
4
Compositor
合成器线程接管
GPU 加速渲染
element.animate() 解剖
element.animate() — 完整参数说明typescript
const animation = element.animate(
// 参数 1: Keyframes — 关键帧定义
[
{ transform: 'translateX(-100%)', opacity: 0 }, // ← 起始帧 (0%)
{ transform: 'translateX(0)', opacity: 1 }, // ← 结束帧 (100%)
],
// 参数 2: KeyframeAnimationOptions — 时间配置
{
duration: 400, // ← 持续时间 (ms)
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', // ← 缓动函数(支持 CSS 不支持的越界值)
fill: 'forwards', // ← fill 模式: none | forwards | backwards | both
iterations: 1, // ← 迭代次数,Infinity = 无限循环
direction: 'normal', // ← normal | reverse | alternate | alternate-reverse
delay: 0, // ← 延迟 (ms)
endDelay: 0, // ← 结束后延迟 (ms) — CSS 中无等价物!
composite: 'replace', // ← 合成模式: replace | add | accumulate
iterationComposite: 'replace', // ← 迭代间的合成模式
id: 'slide-in', // ← 动画标识符,可用于 getAnimations({id: 'slide-in'})
}
);
// 返回的 Animation 对象拥有完整生命周期控制
animation.play(); // ← 开始/继续播放
animation.pause(); // ← 暂停
animation.reverse(); // ← 反转方向
animation.finish(); // ← 立即跳到结束
animation.cancel(); // ← 取消并移除
animation.commitStyles(); // ← 将当前样式写入元素(关键!)
animation.updatePlaybackRate(2); // ← 动态变速 2x
// 精确 seek
animation.currentTime = 200; // ← 跳到 200ms 位置
// Promise 化等待
await animation.finished; // ← 动画完成时 resolve
console.log('动画播放完毕!');FLIP 动画技巧详解
FLIP 动画 — 生产级实现typescript
function flipAnimate(
element: HTMLElement,
mutation: () => void,
options: { duration?: number; easing?: string } = {}
) {
const { duration = 400, easing = 'cubic-bezier(0.34, 1.56, 0.64, 1)' } = options;
// F — First: 记录旧位置
const firstRect = element.getBoundingClientRect();
// L — Last: 执行 DOM 变更,获取新位置
mutation();
const lastRect = element.getBoundingClientRect();
// I — Invert: 计算偏移量
const deltaX = firstRect.left - lastRect.left;
const deltaY = firstRect.top - lastRect.top;
const deltaW = firstRect.width / lastRect.width;
const deltaH = firstRect.height / lastRect.height;
// P — Play: 使用 WAAPI 执行动画
const animation = element.animate(
[
{
transform: `translate(${deltaX}px, ${deltaY}px)
scale(${deltaW}, ${deltaH})`,
},
{ transform: 'none' },
],
{ duration, easing, fill: 'forwards' }
);
// commitStyles — 将最终状态写入元素,防止动画结束后闪烁
animation.finished.then(() => {
animation.commitStyles(); // ← 关键:写入样式
animation.cancel(); // ← 清理 animation 对象
});
return animation;
}
// 使用示例:重排列表
function reorderList(listEl: HTMLElement, newIndex: number) {
const items = Array.from(listEl.children) as HTMLElement[];
const activeItem = items[0]; // 假设移动第一个
flipAnimate(activeItem, () => {
// 在 mutation 回调中执行 DOM 操作
listEl.insertBefore(activeItem, items[newIndex] || null);
}, { duration: 350 });
}Lab
交互式实验场
FLIP Animation Lab
1
2
3
4
5
6
7
8
9
Generated FLIP Code
// Capture → Invert → Play → Last
const first = el.getBoundingClientRect(); // ← First
el.classList.toggle('grid'); // ← trigger layout change
const last = el.getBoundingClientRect(); // ← Last
const deltaX = first.left - last.left; // ← Invert
const deltaY = first.top - last.top;
el.animate([ // ← Play!
{ transform: `translate(${deltaX}px, ${deltaY}px)` },
{ transform: 'translate(0, 0)' }
], {
duration: 400,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
fill: 'forwards'
});Easing & Timing Explorer
cubic-bezier(0.34, 1.56, 0.64, 1)
L4 · Practice
代码实战:从入门到生产
基础:入场/退场动画
fadeSlideIn — 通用入场动画 Hooktypescript
import { useRef, useEffect } from 'react';
function useFadeSlideIn(options?: KeyframeAnimationOptions) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
if (!ref.current) return;
const anim = ref.current.animate(
[
{ opacity: 0, transform: 'translateY(24px) scale(0.96)' },
{ opacity: 1, transform: 'translateY(0) scale(1)' },
],
{
duration: 500,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
fill: 'forwards',
...options,
}
);
// 组件卸载时自动清理
return () => anim.cancel();
}, []);
return ref;
}
// 使用 — 注意:0KB 额外依赖
function ProductCard({ product }: { product: Product }) {
const cardRef = useFadeSlideIn({ delay: 100 });
return (
<div ref={cardRef} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
</div>
);
}进阶:序列编排 & 动态控制
animateSequence — 串联 & 并行动画编排typescript
// 串联播放:一个结束后再播下一个
async function animateSequence(animations: Animation[]) {
for (const anim of animations) {
anim.play();
await anim.finished; // ← .finished 是原生 Promise!
}
}
// 并行播放:同时播,等全部结束
async function animateParallel(animations: Animation[]) {
animations.forEach(a => a.play());
await Promise.all( // ← Promise.all 等待所有完成
animations.map(a => a.finished)
);
}
// 交错 (Stagger) 动画 — 列表项依次出现
function staggerFadeIn(elements: HTMLElement[], staggerMs = 80) {
const animations = elements.map((el, i) =>
el.animate(
[
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{
duration: 400,
delay: i * staggerMs, // ← 逐个递增延迟
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
fill: 'forwards',
}
)
);
// 返回 master animation 控制器
return {
play: () => animations.forEach(a => a.play()),
pause: () => animations.forEach(a => a.pause()),
reverse: () => animations.forEach(a => a.reverse()),
cancel: () => animations.forEach(a => a.cancel()),
get finished() {
return Promise.all(animations.map(a => a.finished));
},
};
}
// 使用
const items = document.querySelectorAll('.list-item') as NodeListOf<HTMLElement>;
const master = staggerFadeIn(Array.from(items), 60);
master.finished.then(() => console.log('所有列表项已入场!'));高级:滚动驱动动画 (ScrollTimeline)
⚠️ 浏览器兼容性:ScrollTimeline 在 Chrome 115+、Edge 115+、Firefox 110+ 中支持。 Safari 17.4+ 部分支持。生产环境建议搭配@supports 检测。
scrollProgress — 滚动进度条 + 元素出场typescript
// 方案 A: 原生 ScrollTimeline(推荐,零依赖)
function initScrollAnimations() {
const progressBar = document.querySelector('.progress-bar') as HTMLElement;
const hero = document.querySelector('.hero') as HTMLElement;
if (!progressBar || !hero) return;
// ① 顶部进度条 — 随页面滚动填充
progressBar.animate(
{ scaleX: ['0', '1'] },
{
timeline: new ScrollTimeline({ // ← 原生 ScrollTimeline
source: document.documentElement, // ← 滚动容器
axis: 'block', // ← block = Y轴
}),
fill: 'both', // ← 关键:双向 fill
}
);
// ② 视口进入时触发动画(用 ViewTimeline)
document.querySelectorAll('.reveal-on-scroll').forEach((el) => {
el.animate(
{
opacity: [0, 1, 1, 0], // ← 0% 进入 → 完全可见 → 开始退出 → 消失
transform: ['translateY(60px)', 'translateY(0)', 'translateY(0)', 'translateY(-30px)'],
},
{
timeline: new ViewTimeline({
subject: el as Element,
axis: 'block',
}),
fill: 'both',
rangeStart: { rangeName: 'cover', offset: CSS.percent(0) },
rangeEnd: { rangeName: 'cover', offset: CSS.percent(100) },
}
);
});
}
// 方案 B: Fallback — 用 IntersectionObserver + WAAPI
function initScrollFallback() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.animate(
[
{ opacity: 0, transform: 'translateY(30px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{ duration: 600, easing: 'ease-out', fill: 'forwards' }
);
observer.unobserve(entry.target); // ← 只触发一次
}
});
},
{ threshold: 0.15 } // ← 15% 可见时触发
);
document.querySelectorAll('.reveal-on-scroll').forEach((el) => {
observer.observe(el);
});
}
// 渐进增强
if ('ScrollTimeline' in window) {
initScrollAnimations();
} else {
initScrollFallback(); // ← 安全降级
}关键 API:commitStyles() 的正确用法
commitStyles — 防止动画结束后的样式闪烁typescript
// 问题:WAAPI 默认使用 fill: 'forwards',但动画结束后
// 元素的 computedStyle 并不会真正改变!
// getComputedStyle(el).opacity 仍然是 0 而非动画后的 1
// ❌ 错误做法 — 依赖 fill: 'forwards' 维持状态
const anim = el.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 300, fill: 'forwards' } // ← 虽然视觉上是 1,但 DOM 属性仍是 0
);
// 如果后续有 CSS transition,会导致从 0 跳到 1
// ✅ 正确做法 — 用 commitStyles 固化最终状态
const anim = el.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 300, fill: 'forwards' }
);
anim.finished.then(() => {
anim.commitStyles(); // ← 将 opacity:1 写入元素的 style 属性
anim.cancel(); // ← 移除动画效果,现在 style 属性已接管
});
// 现在 el.style.opacity === '1'
// 后续的 CSS transition 可以正常从 opacity:1 开始生产级模式:prefers-reduced-motion 适配
safeAnimate — 无障碍友好的动画封装typescript
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
function safeAnimate(
element: HTMLElement,
keyframes: Keyframe[],
options: KeyframeAnimationOptions
): Animation {
// 尊重用户的无障碍偏好
if (prefersReducedMotion) {
return element.animate(keyframes, {
...options,
duration: 0, // ← 立即完成,无过渡
delay: 0,
});
}
// 使用 will-change 提示浏览器优化
element.style.willChange = 'transform, opacity'; // ← GPU 层提升
const anim = element.animate(keyframes, options);
anim.finished.then(() => {
anim.commitStyles();
anim.cancel();
element.style.willChange = 'auto'; // ← 清理,释放 GPU 内存
});
return anim;
}L5 · Pitfalls
常见陷阱与反模式
L5 · Engineering
工程全景:性能数据与选型建议
性能基准测试 · 1000 个 DOM 元素同时动画
CSS transform (合成器)~16ms/frame · GPU 合成器线程
60 FPS
WAAPI transform (合成器)~16ms/frame · GPU 合成器线程
60 FPS
GSAP transform (JS)~17ms/frame · 主线程 rAF
58 FPS
JS rAF + style.left~42ms/frame · 触发重排
24 FPS
测试环境: Chrome 121, MacBook Pro M2, 1000 个 div 同时执行 transform translateX 动画。 数据来源: Chrome DevTools Performance Panel.
选型决策树
只需要入场/退场动画,无运行时控制?→ CSS animation / transition0KB
需要暂停/反转/seek/动态变速?→ Web Animations API0KB
需要复杂序列编排 + morphing + 路径动画?→ GSAP (专业级)~28KB
React 组件级动画 + 手势交互?→ Framer Motion / Motion One~12KB
滚动驱动动画,无依赖?→ WAAPI + ScrollTimeline0KB
Reference
速查清单 (Cheat Sheet)
开始用 WAAPI 动起来
打开浏览器 DevTools Console,输入document.body.animate([{opacity:0},{opacity:1}], 500)即可看到你的第一个 WAAPI 动画。零配置,零依赖。
MDN Web Animations API