
咱们来聊一件有意思的事:2016年Chrome浏览器实现了Web Animations API,但到现在已经8年了,大多数开发者可能还没听说过它。相比之下,CSS动画、Framer Motion、GSAP这些"老朋友"已经被用到烂熟了。但这并不代表Web Animations API不重要------实际上,它可能代表了浏览器原生动画的未来方向。
那么问题来了:这个被忽视的API到底有什么能耐?为什么我们应该改变对它的态度?
重新理解动画的三代演进
在深入代码之前,咱们需要搞清楚动画在浏览器里是怎么进化的:
go
第一代(2008-2012):jQuery时代
├─ setInterval/setTimeout 循环更新
├─ 性能差,容易卡顿(主线程堵塞)
└─ 代码冗余,难以维护
↓
第二代(2012-2018):CSS动画统治
├─ 声明式语法(@keyframes)
├─ GPU加速(off main thread)
├─ 但是!控制能力有限,需要预定义样式
└─ 动态场景需要大量JavaScript+CSS配合
↓
第三代(2018-现在):Web Animations API
├─ JavaScript的声明式动画(最好的两个世界)
├─ GPU加速(和CSS一样快)
├─ 完全的动态控制(和jQuery一样灵活)
└─ 可编程、可组合、可与应用状态同步
这就是为什么说Web Animations API代表了"真正的未来"------它把CSS的性能优势和JavaScript的灵活性结合在一起了。
Web Animations API的本质:让动画成为对象
咱们来看最简单的例子:
go
const box = document.querySelector('.box');
// 调用 animate() 方法
const animation = box.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{
duration: 1000,
fill: 'forwards'
}
);
看起来很简单对不对?但这里的关键点是:**animate() 方法返回了一个 Animation 对象**。这个对象不是字符串,不是Promise,是一个实实在在的对象,它有自己的状态、属性和方法。
这就像把动画从"看不见摸不着的声明"变成了"可以握在手里的东西"。
动画对象内部的秘密
当你调用 element.animate() 的时候,浏览器内部发生了什么?
go
animate() 方法调用
↓
创建 Animation 对象
↓
解析 keyframes 数组
↓
计算关键帧之间的插值
↓
创建 AnimationEffect(描述怎么动)
↓
绑定到元素的动画栈
↓
启动动画循环
├─ 如果可以 GPU 加速 → compositor 线程
└─ 如果不行 → main thread(但会自动降级)
这个过程中最关键的一点是:浏览器会自动判断这个动画能否通过GPU加速。哪些属性可以GPU加速?主要是:
-
transform(2D/3D变换) -
opacity(透明度) -
filter(滤镜) -
某些特定的CSS属性
而颜色、字体大小这些属性,浏览器就会降级到主线程处理。这就是为什么我们经常看到"不要在动画里改background-color"这样的忠告。
动画控制的终极武器
Web Animations API的真正强大之处在于实时控制。来看一个中国开发者经常遇到的场景:
场景1:用户交互中断动画
比如你在ByteDance做某个短视频推荐卡片的动画效果。用户可能快速划动手指中断动画,你需要:
-
立即暂停当前动画
-
读取当前状态
-
从当前位置开始新动画
用CSS?这几乎不可能。用Web Animations API?看代码:
go
class CardAnimation {
constructor(element) {
this.element = element;
this.currentAnimation = null;
}
// 开始滑动出的动画
animateExit(direction = 'right') {
// 如果有正在进行的动画,先清理
if (this.currentAnimation) {
this.currentAnimation.cancel();
}
const startX = this.element.offsetLeft;
const endX = direction === 'right'
? window.innerWidth + 200
: -200;
this.currentAnimation = this.element.animate(
[
{ transform: `translateX(0px)` },
{ transform: `translateX(${endX - startX}px)` }
],
{
duration: 300,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
fill: 'forwards'
}
);
// 监听动画完成
returnthis.currentAnimation.finished;
}
// 关键:中断动画的能力
interruptAndReturn() {
if (this.currentAnimation) {
// 读取当前进度
const progress = this.currentAnimation.currentTime /
this.currentAnimation.effect.getTiming().duration;
// 暂停
this.currentAnimation.pause();
// 从当前位置反向动画回来
const keyframes = [
{
transform: this.element.style.transform,
offset: progress
},
{
transform: 'translateX(0px)',
offset: 1
}
];
this.currentAnimation.cancel();
this.currentAnimation = this.element.animate(keyframes, {
duration: (1 - progress) * 300,
easing: 'ease-out'
});
}
}
}
// 使用
const card = new CardAnimation(document.querySelector('.card'));
// 用户快速划动
document.addEventListener('touchmove', () => {
card.interruptAndReturn();
});
看到了吗?这种交互层面的动画控制,CSS根本做不到。
场景2:多个动画的精确同步
在阿里的eToys或某些电商平台,你可能需要:页面加载时,导航栏、商品图片、价格信息依次出现,每个元素的动画时间要精确同步。
go
class SequenceAnimator {
constructor(elements) {
this.elements = elements;
this.animations = [];
}
async playSequence(delay = 200) {
// 清理之前的动画
this.animations.forEach(anim => anim.cancel());
this.animations = [];
for (let i = 0; i < this.elements.length; i++) {
const element = this.elements[i];
// 创建动画但先不播放
const anim = element.animate(
[
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{
duration: 600,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'// bounce effect
}
);
// 暂停,等待时机
anim.pause();
this.animations.push(anim);
// 计算播放时间
const playTime = i * delay;
// 使用 currentTime 精确控制
setTimeout(() => {
anim.play();
}, playTime);
}
// 返回最后一个动画的完成Promise
returnthis.animations[this.animations.length - 1].finished;
}
// 这才是关键:我们能精确知道每个动画的完成时刻
getCompletionTime() {
returnthis.animations.length * 200 + 600;
}
}
// 使用
const animator = new SequenceAnimator(
document.querySelectorAll('.item')
);
animator.playSequence(150).then(() => {
console.log('所有动画完成!现在可以做其他事了');
// 比如:加载更多内容、展示CTA按钮等
});
这种用代码精确同步多个动画的能力,是CSS animation根本不可能做到的。
性能对比:Web Animations vs CSS vs jQuery
咱们来看看在真实场景中的性能对比。在Tencent某个内部项目里,团队做过一个对比测试:
测试场景:同时动画化200个DOM元素(类似列表项的进入效果)
go
// 场景1:CSS animation(预定义关键帧)
// CSS中:
// @keyframes slideIn {
// from { opacity: 0; transform: translateX(-20px); }
// to { opacity: 1; transform: translateX(0); }
// }
// JavaScript中只是添加类
elements.forEach((el, i) => {
setTimeout(() => {
el.classList.add('animate-slide-in');
}, i * 10);
});
// 场景2:Web Animations API(动态生成)
elements.forEach((el, i) => {
el.animate(
[
{ opacity: 0, transform: 'translateX(-20px)' },
{ opacity: 1, transform: 'translateX(0)' }
],
{
delay: i * 10,
duration: 300,
easing: 'ease-out'
}
);
});
// 场景3:jQuery animate(主线程)
elements.forEach((el, i) => {
setTimeout(() => {
jQuery(el).animate(
{ opacity: 1 },
{ duration: 300 }
);
}, i * 10);
});
结果:
| 方案 | 帧率 | 内存占用 | 代码复杂度 | 动态控制 |
|---|---|---|---|---|
| CSS Animation | 60fps | ~15MB | 低 | 不可能 |
| Web Animations | 58-60fps | ~18MB | 低 | ✅ 完全可能 |
| jQuery | 20-30fps | ~25MB | 中 | ✅ 可能但难 |
关键发现 :Web Animations API的性能几乎等同于CSS animation,但提供了CSS完全做不到的控制能力。
理解缓动函数的深度
Web Animations API不仅支持CSS那些标准缓动值(linear、ease-in-out等),还支持自定义三次贝塞尔曲线。这对想要精细化动画体验的开发者来说很关键。
咱们来看一个真实的场景:Meituan外卖的"加入购物车"动画。这个动画需要有特定的节奏感------开始快速,中间加速,最后缓冲。
go
class CartAnimator {
// 自定义缓动函数库
static easing = {
// 快速开始,缓冲结束(标准)
standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
// 加速,用于"飞入"效果
accelerate: 'cubic-bezier(0.3, 0.0, 0.8, 0.15)',
// Meituan风格:快速振荡进入
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
// 缓慢开始,快速结束(反向加速)
decelerate: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)'
};
// 动画:从商品位置飞到购物车
animateToCart(fromElement, toElement) {
const fromRect = fromElement.getBoundingClientRect();
const toRect = toElement.getBoundingClientRect();
// 计算起点和终点
const startX = fromRect.left + fromRect.width / 2;
const startY = fromRect.top + fromRect.height / 2;
const endX = toRect.left + toRect.width / 2;
const endY = toRect.top + toRect.height / 2;
// 创建临时浮层(类似微信支付时的红包飞行)
const floatingElement = document.createElement('div');
floatingElement.style.cssText = `
position: fixed;
left: ${startX}px;
top: ${startY}px;
width: 40px;
height: 40px;
background: red;
border-radius: 50%;
pointer-events: none;
z-index: 9999;
`;
document.body.appendChild(floatingElement);
// 关键:使用Web Animations API实现复杂的曲线运动
const animation = floatingElement.animate(
[
{
transform: 'translate(0, 0) scale(1)',
opacity: 1
},
// 中途关键帧(制造弧线效果)
{
transform: `translate(${(endX - startX) * 0.5}px, ${(endY - startY) * 0.3}px) scale(0.8)`,
opacity: 0.8,
offset: 0.5
},
{
transform: `translate(${endX - startX}px, ${endY - startY}px) scale(0.3)`,
opacity: 0
}
],
{
duration: 500,
easing: CartAnimator.easing.bounce,
fill: 'forwards'
}
);
// 动画完成后清理
animation.onfinish = () => {
floatingElement.remove();
// 触发购物车震动反馈
this.cartShake(toElement);
};
}
// 购物车抖动效果
cartShake(element) {
element.animate(
[
{ transform: 'scale(1) rotate(0deg)' },
{ transform: 'scale(1.1) rotate(-2deg)' },
{ transform: 'scale(1.05) rotate(2deg)' },
{ transform: 'scale(1) rotate(0deg)' }
],
{
duration: 300,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
iterations: 1
}
);
}
}
// 实际使用
const cartBtn = document.querySelector('.add-to-cart');
const cartIcon = document.querySelector('.cart-icon');
cartBtn.addEventListener('click', function() {
CartAnimator.animateToCart(this, cartIcon);
});
看到了吗?这种精细的动画编排,包括:
-
中途关键帧的精确控制
-
复杂的三维变换
-
动画完成后的级联效果
-
完全通过代码驱动
这些都是CSS animation做不到的,但Web Animations API可以优雅地处理。
深入理解填充模式(fill-mode)
初学者经常忽视 fill 属性,但这个属性在控制动画完成后的状态时至关重要:
go
const element = document.querySelector('.element');
// fill: 'none' - 默认值,动画完成后回到原始状态
element.animate([...], { fill: 'none' });
// 结果:闪现回去(通常不是想要的)
// fill: 'forwards' - 保留最后一帧
element.animate([...], { fill: 'forwards' });
// 结果:留在动画的结束位置(最常用)
// fill: 'backwards' - 从第一帧开始(即使有延迟)
element.animate([...], {
delay: 500,
fill: 'backwards'
});
// 结果:元素先跳到第一帧,延迟500ms后才开始动
// fill: 'both' - 两边都填充
element.animate([...], {
delay: 500,
fill: 'both'
});
// 结果:延迟前显示第一帧,完成后保留最后一帧
在实际项目中,这个属性关乎整个动画序列的视觉连贯性。比如在Alibaba某个项目中,卡片列表的进入动画:
go
// ❌ 错误做法
elements.forEach((el, i) => {
el.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{
delay: i * 100,
duration: 400
// 没有指定 fill
}
);
});
// 问题:延迟期间元素可见,看起来不协调
// ✅ 正确做法
elements.forEach((el, i) => {
el.animate(
[
{ opacity: 0 },
{ opacity: 1 }
],
{
delay: i * 100,
duration: 400,
fill: 'both'// 关键!
}
);
});
// 结果:元素在延迟期间保持透明,动画后保持不透明
Promise和动画链的真正威力
Web Animations API 的 finished 属性返回一个Promise,这让动画可以像异步流程一样处理。这在构建复杂的UI流程时特别有用:
go
class ModalAnimator {
// 模态框进入动画
async open(modalElement) {
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop';
document.body.appendChild(backdrop);
// 第一步:背景淡入
const backdropAnim = backdrop.animate(
[
{ opacity: 0 },
{ opacity: 0.5 }
],
{
duration: 300,
fill: 'forwards'
}
);
// 第二步:等待背景动画完成
await backdropAnim.finished;
// 第三步:模态框从下方滑入
const modalAnim = modalElement.animate(
[
{
transform: 'translateY(100%)',
opacity: 0
},
{
transform: 'translateY(0)',
opacity: 1
}
],
{
duration: 350,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
fill: 'forwards'
}
);
await modalAnim.finished;
console.log('模态框完全打开');
}
async close(modalElement) {
const backdrop = document.querySelector('.modal-backdrop');
// 反向流程
const modalAnim = modalElement.animate(
[
{
transform: 'translateY(0)',
opacity: 1
},
{
transform: 'translateY(100%)',
opacity: 0
}
],
{
duration: 300,
easing: 'ease-in',
fill: 'forwards'
}
);
await modalAnim.finished;
const backdropAnim = backdrop.animate(
[
{ opacity: 0.5 },
{ opacity: 0 }
],
{
duration: 250,
fill: 'forwards'
}
);
await backdropAnim.finished;
backdrop.remove();
console.log('模态框完全关闭');
}
}
// 使用方式很直观
const modal = new ModalAnimator();
const modalEl = document.querySelector('.modal');
// 打开
await modal.open(modalEl);
// 用户交互后关闭
closeBtn.addEventListener('click', async () => {
await modal.close(modalEl);
});
这种异步流程的能力,让复杂的动画序列变成了可读的、可维护的代码。对比一下传统的嵌套callback地狱或setTimeout计时:
go
// ❌ 传统做法(难以维护)
backdropElement.style.animation = 'fadeIn 300ms ease-out forwards';
setTimeout(() => {
modalElement.style.animation = 'slideUp 350ms cubic-bezier(...) forwards';
setTimeout(() => {
console.log('打开');
}, 350);
}, 300);
// ✅ Web Animations API(可读性好)
await backdrop.animate([...]).finished;
await modal.animate([...]).finished;
console.log('打开');
浏览器兼容性和降级方案
现在的问题是:Web Animations API在生产环境下能用吗?
go
Chrome/Edge: ✅ 2016年就支持了
Firefox: ✅ 2018年开始支持
Safari: ⚠️ MacOS支持,iOS Safari 13.4+才支持
IE: ❌ 完全不支持
如果你的用户群体包括大量iOS用户,你需要一个降级方案。幸运的是,Web Animations API有一个官方的polyfill:
go
<!-- 条件加载polyfill -->
<script>
if (!Element.prototype.animate) {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/web-animations/2.3.2/web-animations.min.js';
document.head.appendChild(script);
}
</script>
或者使用更聪明的降级方案:
go
class AnimationCompat {
static canUseNative() {
returntypeof Element.prototype.animate === 'function';
}
static animate(element, keyframes, options) {
if (this.canUseNative()) {
// 使用原生API
return element.animate(keyframes, options);
} else {
// 降级到CSS animation
returnthis.animateWithCSS(element, keyframes, options);
}
}
static animateWithCSS(element, keyframes, options) {
// 动态生成关键帧
const keyframeName = `anim-${Date.now()}`;
const keyframeCSS = this.generateKeyframeCSS(keyframeName, keyframes);
// 注入样式
const style = document.createElement('style');
style.textContent = keyframeCSS;
document.head.appendChild(style);
// 应用动画
element.style.animation = `${keyframeName} ${options.duration}ms ${options.easing || 'ease'} ${options.fill || 'forwards'}`;
// 返回兼容的Animation对象(简化版)
return {
play: () => { element.style.animationPlayState = 'running'; },
pause: () => { element.style.animationPlayState = 'paused'; },
cancel: () => { element.style.animation = 'none'; },
finished: newPromise(resolve => {
setTimeout(resolve, options.duration);
})
};
}
static generateKeyframeCSS(name, keyframes) {
let css = `@keyframes ${name} { `;
keyframes.forEach(frame => {
const offset = frame.offset !== undefined ? frame.offset * 100 : '';
css += offset ? `${offset}% {` : `{`;
Object.keys(frame).forEach(key => {
if (key !== 'offset') {
css += `${this.camelToKebab(key)}: ${frame[key]};`;
}
});
css += `} `;
});
css += `}`;
return css;
}
static camelToKebab(str) {
return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
}
}
// 使用
const anim = AnimationCompat.animate(element, [...], { duration: 300 });
await anim.finished;
为什么很多项目没采用Web Animations API
在我和一些中国大厂的开发者聊天中,发现不采用的主要原因是:
-
信息差:很多人根本不知道这个API的存在和能力
-
习惯性:已经习惯了CSS动画和第三方库(Framer Motion、GSAP)
-
文档不足:中文文档很少,学习曲线看起来陡峭
-
误解:以为它就是CSS animation的JavaScript版本,没有本质优势
但实际上,如果你需要动态控制、精确同步、复杂交互 ,Web Animations API是目前浏览器提供的最强大、最标准、最有效率的方案。
性能监测:确保你的动画真的快
不要盲目相信"Web Animations很快"的说法。在生产环境中,你需要实际测量:
go
class AnimationProfiler {
staticasync profileAnimation(element, keyframes, options, label = 'Animation') {
// 记录帧率
let frameCount = 0;
let lastTime = performance.now();
const frameTimes = [];
const countFrames = () => {
frameCount++;
const now = performance.now();
frameTimes.push(now - lastTime);
lastTime = now;
if (frameCount < 300) { // 最多监测5秒(60fps下)
requestAnimationFrame(countFrames);
}
};
// 开始计数
requestAnimationFrame(countFrames);
// 开始动画
const animation = element.animate(keyframes, options);
const startMem = performance.memory?.usedJSHeapSize || 0;
// 等待完成
await animation.finished;
const endMem = performance.memory?.usedJSHeapSize || 0;
// 分析结果
const avgFrameTime = frameTimes.reduce((a, b) => a + b) / frameTimes.length;
const fps = 1000 / avgFrameTime;
const memDelta = endMem - startMem;
console.log(`📊 ${label} 性能分析:`);
console.log(` 帧率: ${fps.toFixed(1)} fps`);
console.log(` 平均帧时间: ${avgFrameTime.toFixed(2)}ms`);
console.log(` 内存增长: ${(memDelta / 1024 / 1024).toFixed(2)}MB`);
console.log(` 掉帧: ${frameTimes.filter(t => t > 16.67).length} 次`);
return { fps, frameTimes, memDelta };
}
}
// 使用
const element = document.querySelector('.element');
await AnimationProfiler.profileAnimation(
element,
[
{ opacity: 0, transform: 'translateX(-100px)' },
{ opacity: 1, transform: 'translateX(0)' }
],
{ duration: 500, easing: 'ease-out' },
'进入动画'
);
总结:Web Animations API为什么是未来
go
特性对比表:
Web Animations | CSS Animation | GSAP | Framer Motion
─────────────────────────────────────────────────────────────────────
原生支持 ✅ ✅ ❌ ❌
性能(GPU) ✅ ✅ ✅ ✅
动态控制 ✅ ❌ ✅ ✅
组合能力 ✅ ❌ ✅ ✅
状态同步 ✅ ❌ ✅ ✅
代码复杂度 中 低 中 中
学习成本 中 低 高 中
包体积 0KB 0KB 30KB 100KB+
Web Animations API 的真正价值在于:
-
零依赖:不需要任何第三方库,直接用浏览器提供的API
-
完全控制:能做到CSS和jQuery都做不到的精细控制
-
性能最优:和CSS animation一样快,但灵活得多
-
标准化:是W3C标准,不会因为某个库的维护问题而困扰
-
易于组合:动画可以像对象一样处理和组合
如果你正在:
-
构建交互复杂的UI(购物车动画、模态框序列)
-
需要根据用户交互精确控制动画
-
想要在关键路径上减少JavaScript包体积
-
构建需要精确同步多个动画的应用
那么Web Animations API就是你应该优先考虑的方案。
不要让信息差束缚你的想象力。浏览器提供了强大的工具,只是需要有人去用。
FAQ
Q: Web Animations API和GSAP相比,为什么要用前者?
A: 如果你的动画需求是:简单过渡、列表进入、弹窗动画等常见场景,Web Animations API完全够用。GSAP的优势在于Timeline控制、插件生态和跨浏览器支持,但成本是多了30KB的包体积。选择哪个,看你的需求和项目约束。
Q: Web Animations API支持SVG动画吗?
A: 完全支持!SVG元素也有animate()方法,可以动画化任何SVG属性(x、y、cx、cy、r等)。实际上在某些场景下,Web Animations API是SVG动画最灵活的方案。
Q: 如果浏览器不支持,我该怎么办?
A: 使用polyfill或降级到CSS animation。上面提供的AnimationCompat类展示了完整的降级方案。生产环境中,确保测试iOS Safari和旧版浏览器的表现。
Q: 能否用Web Animations API替代整个动画库?
A: 取决于你的项目复杂度。简单场景完全可以。复杂场景(timeline、精确的点位控制、特殊缓动)可能还是需要GSAP。但即使用GSAP,理解Web Animations API也能帮你写出更好的代码。
Q: cancelAnimationFrame和动画暂停有什么区别?
A: cancelAnimationFrame是停止特定的动画帧回调。animation.pause()是暂停整个动画对象。它们工作在不同的层面。在Web Animations中,用pause()、play()、cancel()来控制,不需要用cancelAnimationFrame。
推荐阅读
-
MDN - Web Animations API
-
W3C Web Animations标准
如果这篇文章对你有帮助,欢迎关注《前端达人》公众号! 🎯
咱们每周分享React、Node.js、TypeScript等前端硬核知识,既有源码级别的深度解析,也有实战项目的最佳实践。在前端这条路上,你不是一个人在奋斗。
还有一件事:如果你发现这篇文章有价值,点个赞、转发给更多的前端开发者吧。分享知识是让我们社区变得更强大的最好方式。
期待在下一篇文章中和你再见!💻✨