动画与过渡
问题
CSS 过渡(transition)和动画(animation)有什么区别?如何实现高性能动画?哪些属性适合做动画?
答案
transition 过渡
transition 在属性值发生变化时产生平滑过渡效果,需要触发条件(如 :hover、class 变化)。
.box {
/* transition: 属性 时长 时间函数 延迟; */
transition: transform 0.3s ease 0s;
/* 多个属性 */
transition: transform 0.3s ease,
opacity 0.3s ease,
background-color 0.2s linear;
/* 所有可过渡属性(性能不如指定属性) */
transition: all 0.3s ease;
}
.box:hover {
transform: scale(1.1);
opacity: 0.8;
}
transition 四个子属性
| 属性 | 说明 | 默认值 |
|---|---|---|
transition-property | 过渡的属性名 | all |
transition-duration | 持续时间 | 0s |
transition-timing-function | 时间函数(缓动曲线) | ease |
transition-delay | 延迟时间 | 0s |
时间函数
.box {
transition-timing-function: ease; /* 慢-快-慢(默认) */
transition-timing-function: linear; /* 匀速 */
transition-timing-function: ease-in; /* 慢-快 */
transition-timing-function: ease-out; /* 快-慢 */
transition-timing-function: ease-in-out; /* 慢-快-慢 */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); /* 自定义 */
transition-timing-function: steps(4, end); /* 步进,逐帧动画 */
}
推荐工具:cubic-bezier.com 可视化调节缓动曲线。
常用预设:
cubic-bezier(0.4, 0, 0.2, 1)— Material Design 标准曲线cubic-bezier(0.68, -0.55, 0.265, 1.55)— 弹性效果(Back)
animation 动画
animation 配合 @keyframes 实现独立、可循环的动画,不需要触发条件。
/* 定义关键帧 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 多关键帧 */
@keyframes bounce {
0% { transform: translateY(0); }
30% { transform: translateY(-30px); }
50% { transform: translateY(0); }
70% { transform: translateY(-15px); }
100% { transform: translateY(0); }
}
.element {
/* animation: 名称 时长 时间函数 延迟 次数 方向 填充模式 播放状态; */
animation: fadeIn 0.5s ease-out;
}
animation 八个子属性
| 属性 | 说明 | 常用值 |
|---|---|---|
animation-name | 关键帧名称 | @keyframes 名 |
animation-duration | 持续时间 | 0.5s、300ms |
animation-timing-function | 缓动函数 | ease、linear |
animation-delay | 延迟 | 0s、0.2s |
animation-iteration-count | 播放次数 | 1、infinite |
animation-direction | 播放方向 | normal、reverse、alternate |
animation-fill-mode | 填充模式 | none、forwards、backwards、both |
animation-play-state | 播放/暂停 | running、paused |
fill-mode 详解
.box {
animation: fadeIn 1s ease;
/* fill-mode 控制动画前后的样式 */
animation-fill-mode: none; /* 默认,动画前后恢复元素原始样式 */
animation-fill-mode: forwards; /* 动画结束后保持最后一帧 */
animation-fill-mode: backwards; /* 延迟期间应用第一帧 */
animation-fill-mode: both; /* forwards + backwards */
}
direction 详解
.box {
animation-direction: normal; /* 正向播放 0% → 100% */
animation-direction: reverse; /* 反向播放 100% → 0% */
animation-direction: alternate; /* 奇数次正向,偶数次反向 */
animation-direction: alternate-reverse; /* 奇数次反向,偶数次正向 */
}
transition vs animation
| 特性 | transition | animation |
|---|---|---|
| 触发条件 | 需要(:hover、class 变化) | 不需要,自动开始 |
| 关键帧 | 只有起始和结束两个状态 | @keyframes 可定义多个 |
| 循环播放 | 不支持 | infinite |
| 暂停/恢复 | 不支持 | animation-play-state |
| JS 控制 | 修改属性值触发 | animationstart/end/iteration 事件 |
| 适用场景 | 简单状态切换 | 复杂、连续动画 |
高性能动画
可以做动画的属性
浏览器渲染管线:
| 层级 | 触发操作 | 属性示例 | 性能 |
|---|---|---|---|
| 合成层(Composite) | 仅合成 | transform、opacity | 🟢 最佳 |
| 绘制层(Paint) | 重绘 + 合成 | color、background、box-shadow | 🟡 中等 |
| 布局层(Layout) | 重排 + 重绘 + 合成 | width、height、margin、top | 🔴 最差 |
/* ❌ 差性能:每帧触发重排 */
.bad {
transition: left 0.3s, top 0.3s, width 0.3s;
}
/* ✅ 好性能:只触发合成 */
.good {
transition: transform 0.3s, opacity 0.3s;
}
位移用 transform: translate() 代替 top/left,缩放用 transform: scale() 代替 width/height。
will-change 提升合成层
.animated {
will-change: transform, opacity; /* 提前告知浏览器该元素会变化 */
}
/* 注意:用完要移除 */
.animated.done {
will-change: auto;
}
- 不要给所有元素加
will-change,会消耗大量内存 - 在动画开始前添加,动画结束后移除
- 过多合成层会导致层爆炸,反而降低性能
GPU 加速技巧
/* 强制开启 GPU 加速(hack,不推荐) */
.gpu-hack {
transform: translateZ(0);
/* 或 */
transform: translate3d(0, 0, 0);
}
/* 推荐:使用 will-change */
.gpu-proper {
will-change: transform;
}
常用动画实例
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-30px); }
60% { transform: translateY(-15px); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #f3f3f3;
border-top: 3px solid #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
Web Animations API (WAAPI)
JS 控制动画的现代标准,性能等同 CSS 动画:
const element = document.querySelector('.box') as HTMLElement;
// 创建动画
const animation = element.animate(
[
{ transform: 'translateY(0)', opacity: 1 },
{ transform: 'translateY(-50px)', opacity: 0 }
],
{
duration: 500,
easing: 'ease-out',
fill: 'forwards'
}
);
// 控制动画
animation.pause();
animation.play();
animation.reverse();
animation.cancel();
// 监听事件
animation.onfinish = () => console.log('动画结束');
animation.finished.then(() => console.log('Promise 版'));
常见面试问题
Q1: transition 和 animation 的区别?
答案:
核心区别:
transition需要触发条件(:hover、JS 改变属性),只有起始和结束两个状态animation自动播放,可通过@keyframes定义多个中间状态,支持循环、暂停
简单交互(悬停变色、展开菜单)用 transition;
复杂动画(加载动画、入场动画)用 animation。
Q2: 哪些 CSS 属性做动画性能好?
答案:
只有 transform 和 opacity 的动画只触发合成,不会重排重绘,性能最佳。
/* ✅ 高性能 */
transform: translate(), scale(), rotate();
opacity: 0 → 1;
/* ❌ 低性能(触发重排) */
width, height, margin, padding, top, left, font-size
原因是 transform 和 opacity 可以在合成线程(compositor thread)独立处理,不阻塞主线程。更多详情参见渲染优化。
Q3: will-change 有什么用?怎么正确使用?
答案:
will-change 提前通知浏览器元素即将变化的属性,浏览器可以做优化准备(如提升到独立合成层)。
/* 正确用法:hover 时才添加 */
.card:hover {
will-change: transform;
}
/* 或通过 JS 在动画前添加,动画后移除 */
错误用法:
* { will-change: transform; }— 资源浪费- 长期保留 — 内存泄漏
- 太多元素 — 合成层爆炸
Q4: CSS 动画和 JS 动画哪个性能好?
答案:
| 方案 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|
| CSS animation | 可在合成线程运行 | 低 | 预定义的简单动画 |
| WAAPI | 同 CSS animation | 中 | 需要 JS 控制的动画 |
requestAnimationFrame | 主线程 | 高 | 复杂逻辑动画 |
setTimeout/setInterval | 最差 | 高 | 不推荐 |
CSS 动画和 WAAPI 在性能上基本等同,都优于手动 JS 动画。但如果 CSS 动画中使用了触发重排的属性,同样性能差。
Q5: animation-fill-mode 的 forwards 和 both 有什么区别?
答案:
forwards:动画结束后保持最后一帧的样式backwards:在延迟期间应用第一帧的样式both:同时具备 forwards 和 backwards 的效果none(默认):动画前后都恢复原始样式
.box {
opacity: 1; /* 原始值 */
animation: fadeOut 1s ease 0.5s forwards;
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* forwards:动画结束后 opacity 保持 0 */
/* none:动画结束后 opacity 恢复为 1 */
Q6: 如何实现逐帧动画(精灵图动画)?
答案:
使用 steps() 时间函数配合精灵图(sprite sheet):
.sprite {
width: 64px;
height: 64px;
background: url('sprite.png') no-repeat;
animation: walk 0.6s steps(8) infinite;
}
@keyframes walk {
from { background-position: 0 0; }
to { background-position: -512px 0; } /* 8帧 × 64px */
}
steps(8) 表示将动画分成 8 步(8 帧),每步瞬间切换,不产生中间过渡。
Q7: 如何暂停和恢复 CSS 动画?
答案:
.animated {
animation: spin 2s linear infinite;
}
.animated.paused {
animation-play-state: paused;
}
// JS 控制
const el = document.querySelector('.animated') as HTMLElement;
el.style.animationPlayState = 'paused'; // 暂停
el.style.animationPlayState = 'running'; // 恢复
Q8: 如何监听 CSS 动画和过渡的事件?
答案:
const el = document.querySelector('.box') as HTMLElement;
// transition 事件
el.addEventListener('transitionstart', (e: TransitionEvent) => {
console.log(`过渡开始: ${e.propertyName}`);
});
el.addEventListener('transitionend', (e: TransitionEvent) => {
console.log(`过渡结束: ${e.propertyName}, 耗时: ${e.elapsedTime}s`);
});
el.addEventListener('transitioncancel', (e: TransitionEvent) => {
console.log('过渡取消');
});
// animation 事件
el.addEventListener('animationstart', (e: AnimationEvent) => {
console.log(`动画开始: ${e.animationName}`);
});
el.addEventListener('animationend', (e: AnimationEvent) => {
console.log('动画结束');
});
el.addEventListener('animationiteration', (e: AnimationEvent) => {
console.log('动画完成一次循环');
});
Q9: 什么是 GPU 加速?为什么 transform 能触发 GPU 加速?
答案:
GPU 加速指将元素提升到独立的合成层(Compositing Layer),由 GPU 直接处理变换和透明度,不需要 CPU 重新布局或绘制。
transform 和 opacity 能触发 GPU 加速是因为它们的变化不影响其他元素的布局和绘制,可以在合成阶段独立处理。
但过多合成层会占用大量显存,会适得其反(层爆炸)。Chrome DevTools → Layers 面板可查看合成层数量。
Q10: prefers-reduced-motion 是什么?为什么要用?
答案:
这是一个无障碍媒体查询,当用户在系统设置中开启了"减少动画"时匹配。前庭功能障碍的用户可能会因为动画感到不适。
/* 默认有动画 */
.card {
animation: fadeIn 0.5s ease;
}
/* 用户偏好减少动画时 */
@media (prefers-reduced-motion: reduce) {
.card {
animation: none;
}
/* 或者缩短动画时间 */
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}