CSS 动画与过渡
为什么动画是现代 UI 的标配
在互联网早期,页面动画几乎是 JavaScript 的领地。今天,CSS 动画已经足够强大,能够处理绝大多数的 UI 动画场景——从简单的 hover 效果到复杂的页面转场。
理解 CSS 动画不只是会写 transition: all 0.3s ease。你需要理解 transition 和 @keyframes animation 的本质区别、各类时序函数的特性、以及哪些 CSS 属性适合做动画、哪些不适合。这些决定了你的动画是否流畅、是否影响性能。
面试定位:CSS 动画在面试中主要考察工程判断。面试官通过询问 transition 和 animation 的选择、哪些属性适合动画化、性能优化策略等问题,判断候选人是否有实际的动画开发和优化经验。
transition 的完整机制
transition 属性分解
transition 是四个属性的简写:
.element {
transition-property: background-color; /* 要过渡的属性 */
transition-duration: 300ms; /* 过渡时长 */
transition-timing-function: ease-in-out; /* 时序函数 */
transition-delay: 100ms; /* 延迟 */
}
/* 简写 */
.element {
transition: background-color 300ms ease-in-out 100ms;
}
/* 多属性过渡 */
.element {
transition:
background-color 300ms ease,
transform 200ms ease-out,
opacity 150ms linear;
}
transition-property 的细节
不是所有 CSS 属性都可以过渡。可以通过以下方式判断:
- 可过渡:数值类属性(width、height、opacity、transform、color 等)
- 不可过渡:display、visibility(但可以用其他技巧模拟)
/* visibility 可以"过渡",但实际上是渐变 */
.element {
transition: opacity 300ms, visibility 0ms 300ms;
/* opacity 变化后 300ms 才切换 visibility */
}
transition-timing-function
| 时序函数 | 特性 |
|---|---|
ease |
开始慢,结尾快(默认值) |
ease-in |
开始慢,结尾快 |
ease-out |
开始快,结尾慢 |
ease-in-out |
开始慢,结尾慢 |
linear |
匀速 |
cubic-bezier(n, n, n, n) |
自定义贝塞尔曲线 |
steps(n) |
阶梯过渡 |
cubic-bezier 自定义曲线
/* 常见的自定义曲线 */
.gentle { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } /* Material Design 标准 */
.snappy { transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } /* 快速启动,缓慢结束 */
.bounce { transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55); } /* 弹性效果 */
transition vs animation 的选择
核心区别
| 特性 | transition | @keyframes animation |
|---|---|---|
| 触发方式 | 状态变化(:hover 等) | 自动播放/JS 控制 |
| 关键帧 | 隐含(起始 + 结束) | 显式定义多个关键帧 |
| 循环 | 否 | 支持(animation-iteration-count) |
| 可暂停 | 否 | 支持(animation-play-state) |
| 复杂轨迹 | 困难 | 容易实现 |
选择原则
/* 适合用 transition 的场景 */
.button:hover {
background-color: darken(#007bff, 10%);
transform: scale(1.05);
}
.fade-in {
opacity: 0;
transition: opacity 300ms ease;
}
.fade-in.visible {
opacity: 1;
}
/* 适合用 animation 的场景 */
/* 1. 需要循环 */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader {
animation: spin 1s linear infinite;
}
/* 2. 需要复杂路径 */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
/* 3. 需要自动播放 */
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes 完整语法
关键帧定义
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 也可以用百分比 */
@keyframes slideIn {
0% {
transform: translateX(-100%);
opacity: 0;
}
80% {
transform: translateX(10px); /* 超过终点 */
}
100% {
transform: translateX(0);
opacity: 1;
}
}
animation 属性详解
.element {
/* 完整属性 */
animation-name: fadeIn;
animation-duration: 300ms;
animation-timing-function: ease-out;
animation-delay: 0ms;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: none;
animation-play-state: running;
/* 简写 */
animation: fadeIn 300ms ease-out 0ms 1 normal none running;
}
animation-fill-mode 的四个值
animation-fill-mode 决定动画在播放前后的样式:
@keyframes scale {
from { transform: scale(1); }
to { transform: scale(1.5); }
}
.box {
transform: scale(1); /* 初始状态 */
animation: scale 1s ease-out;
}
/*
* none: 播放前应用初始状态,播放中应用关键帧,播放后回到初始状态
* forwards: 播放前应用初始状态,播放中应用关键帧,播放后停在最后一帧
* backwards: 播放前应用第一帧样式,播放中应用关键帧,播放后回到初始状态
* both: 播放前应用第一帧样式,播放中应用关键帧,播放后停在最后一帧
*/
animation-play-state
/* 暂停/继续动画 */
.paused {
animation-play-state: paused;
}
.running {
animation-play-state: running;
}
/* 实际应用:悬停时暂停 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.card {
animation: pulse 2s ease-in-out infinite;
}
.card:hover {
animation-play-state: paused; /* 悬停时停止脉动 */
}
动画性能优化
渲染管线与动画成本
浏览器渲染页面的过程:
Style → Layout → Paint → Composite
不同属性的动画成本差异巨大:
| 属性 | 成本 | 原因 |
|---|---|---|
transform |
低 | 只触发 Composite |
opacity |
低 | 只触发 Composite |
filter |
中 | Paint + Composite |
width/height |
高 | 触发 Layout |
background-color |
中 | 触发 Paint |
为什么 transform 和 opacity 最适合动画
这两个属性的变化不会触发布局重计算,只需要在合成阶段处理,非常高效:
/* 高效动画:只触发 Composite */
.highlight {
transition: transform 200ms ease, opacity 200ms ease;
}
/* 低效动画:触发 Layout */
.lowlight {
transition: width 200ms ease, height 200ms ease, background-color 200ms ease;
}
will-change 的正确使用
will-change 提示浏览器提前优化,但滥用会增加内存消耗:
/* 推荐用法:提前声明,适时开启 */
.card {
will-change: transform;
/* 浏览器提前为 transform 创建新的层叠上下文
* 但动画开始前不会真正优化 */
}
.card:hover {
transform: scale(1.05);
/* 动画开始,浏览器使用优化路径 */
}
/* 滥用示例 */
.bad-practice {
will-change: transform, opacity, left, top, width, height;
/* 为所有属性都创建优化上下文,浪费内存 */
}
使用建议
/* 1. 只对需要动画的属性使用 will-change */
.optimized {
will-change: transform;
}
/* 2. 动画结束后移除 will-change(通过 JavaScript) */
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
/* 3. 避免在大量元素上使用 */
.list-item {
will-change: transform; /* 如果列表有 1000 项,这会很糟糕 */
}
实战场景
场景一:按钮交互反馈
.btn {
position: relative;
overflow: hidden;
transition: background-color 200ms ease, transform 100ms ease;
}
.btn:active {
transform: scale(0.97); /* 按下时的反馈 */
}
.btn::after {
content: '';
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.2);
transform: scale(0);
transition: transform 300ms ease;
}
.btn:hover::after {
transform: scale(1); /* hover 时的波纹效果 */
}
场景二:卡片入场动画
@keyframes cardEnter {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.card {
animation: cardEnter 400ms ease-out forwards;
opacity: 0; /* 初始隐藏 */
}
/* 交错动画 */
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 100ms; }
.card:nth-child(3) { animation-delay: 200ms; }
场景三:骨架屏(Skeleton)动画
@keyframes shimmer {
from {
background-position: -200% 0;
}
to {
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;
}
面试高频问题
Q: transition 和 animation 有什么区别?各适合什么场景?
回答要点:transition 需要状态触发(:hover 等),只能处理起始和结束两个状态,适合简单的状态变化。animation 可以自动播放、循环、暂停,适合需要复杂轨迹、循环执行、自动播放的场景。
Q: 哪些 CSS 属性适合做动画?哪些不适合?为什么?
回答要点:transform 和 opacity 性能最好,只触发 Composite 阶段。不适合动画的属性包括 width/height(触发 Layout)、background-color(触发 Paint)、display(不可过渡)。动画性能优化的核心是避免触发 Layout 和 Paint。
Q: will-change 应该怎么用?有什么副作用?
回答要点:will-change 提示浏览器提前为特定属性创建优化上下文。正确用法是只对需要动画的属性使用、在动画开始前声明、在动画结束后清理。副作用包括增加内存消耗、可能导致层叠上下文变化影响 z-index。