跳到主要内容

动画与过渡

问题

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.5s300ms
animation-timing-function缓动函数easelinear
animation-delay延迟0s0.2s
animation-iteration-count播放次数1infinite
animation-direction播放方向normalreversealternate
animation-fill-mode填充模式noneforwardsbackwardsboth
animation-play-state播放/暂停runningpaused

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

特性transitionanimation
触发条件需要(:hover、class 变化)不需要,自动开始
关键帧只有起始和结束两个状态@keyframes 可定义多个
循环播放不支持infinite
暂停/恢复不支持animation-play-state
JS 控制修改属性值触发animationstart/end/iteration 事件
适用场景简单状态切换复杂、连续动画

高性能动画

可以做动画的属性

浏览器渲染管线:

层级触发操作属性示例性能
合成层(Composite)仅合成transformopacity🟢 最佳
绘制层(Paint)重绘 + 合成colorbackgroundbox-shadow🟡 中等
布局层(Layout)重排 + 重绘 + 合成widthheightmargintop🔴 最差
避免触发 Layout 的动画
/* ❌ 差性能:每帧触发重排 */
.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 注意事项
  • 不要给所有元素加 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 属性做动画性能好?

答案

只有 transformopacity 的动画只触发合成,不会重排重绘,性能最佳。

/* ✅ 高性能 */
transform: translate(), scale(), rotate();
opacity: 01;

/* ❌ 低性能(触发重排) */
width, height, margin, padding, top, left, font-size

原因是 transformopacity 可以在合成线程(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-modeforwardsboth 有什么区别?

答案

  • 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 重新布局或绘制。

transformopacity 能触发 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;
}
}

相关链接