CSS 动画指南:过渡、关键帧与性能
CSS 动画为界面注入生命,提供视觉反馈、引导用户注意力并创造令人难忘的体验。做得好的动画感觉自然直观;做得差的动画会导致卡顿、分散用户注意力,甚至引发身体不适。本指南涵盖了从过渡和关键帧的基础知识到高级性能优化和无障碍性的所有内容,让你掌握创建流畅、有目的且包容的动画的知识。
CSS 过渡与动画
CSS 提供了两种创建动效的不同机制:过渡和动画。理解何时使用每种机制是有效进行 CSS 动画工作的基础。
CSS 过渡
过渡为 CSS 属性两个状态之间的变化添加动画。你定义要动画的属性、动画持续时间和缓动曲线,浏览器自动处理起始值和结束值之间的插值。过渡由状态变化触发,如悬停、聚焦、类添加或 JavaScript 属性更改。
.button {
background: #3b82f6;
transform: scale(1);
transition: background 0.3s ease, transform 0.2s ease;
}
.button:hover {
background: #2563eb;
transform: scale(1.05);
}过渡非常适合简单的两态动画。它们需要最少的代码,易于理解,并且在浏览器中可靠运行。限制是它们只在两个值之间做动画——你不能定义中间步骤。
CSS 动画(关键帧)
动画使用 @keyframes 规则定义任意数量步骤的样式变化序列。与过渡不同,动画可以独立于状态变化运行、无限循环、反向播放和暂停。它们让你精确控制动画的每一帧。
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.pulse-element {
animation: pulse 2s ease-in-out infinite;
}何时使用哪种
| 场景 | 使用过渡 | 使用动画 |
|---|---|---|
| 悬停效果 | 是 | 否 |
| 焦点状态 | 是 | 否 |
| 切换可见性 | 是 | 可能 |
| 多步序列 | 否 | 是 |
| 循环动画 | 否 | 是 |
| 独立于状态 | 否 | 是 |
| 入场动画 | 可能 | 是 |
关键帧语法深入解析
@keyframes 规则是 CSS 动画的核心。充分理解其语法可以释放多步动画序列的全部力量。
基于百分比的关键帧
关键帧使用从 0% 到 100% 的百分比值定义,表示动画时间线上的进度。你可以根据需要定义任意数量的关键帧,浏览器在它们之间进行插值:
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
25% {
transform: translateY(-20px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-10px);
}
}多个关键帧可以通过逗号分隔百分比值来共享同一个样式块,如上面示例中的 0%, 100%。这对于返回起始状态的动画很有用。
动画简写属性
animation 简写属性将八个子属性组合成一个声明。顺序为:name、duration、timing-function、delay、iteration-count、direction、fill-mode 和 play-state。
.element {
animation: slideIn 0.5s ease-out 0.2s 1 forwards running;
}
/* Equivalent longhand */
.element {
animation-name: slideIn;
animation-duration: 0.5s;
animation-timing-function: ease-out;
animation-delay: 0.2s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}动画填充模式
animation-fill-mode 属性控制动画运行前后应用哪些样式。这是最常被误解的动画属性之一:
- none(默认):动画结束时元素返回到动画前的样式。
- forwards:动画结束后元素保留最后一帧的样式。对于入场动画中希望元素停留在最终位置的情况至关重要。
- backwards:在动画延迟期间(动画开始前),元素应用第一帧的样式。
- both:结合 forwards 和 backwards 行为。元素在延迟期间显示第一帧,完成后保留最后一帧。
缓动函数
缓动函数(也称为定时函数)定义动画的加速曲线。它们决定动画是缓慢开始然后加速、快速开始然后减速,还是遵循更复杂的模式。正确的缓动函数使动画感觉自然;错误的缓动函数使动画感觉机械或突兀。
内置缓动函数
- linear:整个动画过程中速度恒定。很少看起来自然,因为现实世界中的物体不会匀速运动。适用于机械或旋转动画。
- ease(默认):开始缓慢,中间加速,结尾减速。适用于大多数 UI 过渡的良好通用缓动。
- ease-in:开始缓慢然后加速。创造「积蓄动量」的感觉。适用于元素离开屏幕或下落。
- ease-out:开始快速然后减速。创造「就位」的感觉。UI 元素进入屏幕时最常用的缓动。
- ease-in-out:慢速开始和结束,中间更快。类似于 ease 但加速和减速更明显。适用于更大、更戏剧性的过渡。
三次贝塞尔曲线
cubic-bezier() 函数让你通过精确控制定义自定义缓动曲线。它接受四个参数表示三次贝塞尔曲线的控制点:cubic-bezier(x1, y1, x2, y2)。x 值表示时间(0 到 1),y 值表示进度(可以超出 0-1 实现过冲效果)。
.element {
/* Smooth deceleration - great for entrances */
transition: transform 0.3s cubic-bezier(0.0, 0.0, 0.2, 1.0);
/* Smooth acceleration - great for exits */
transition: transform 0.2s cubic-bezier(0.4, 0.0, 1, 1);
/* Material Design standard easing */
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}阶梯函数
steps() 函数创建离散的逐帧动画,而非平滑插值。这对于精灵图动画或创建「打字」效果很有用:
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
.typing-effect {
animation: typing 3s steps(40, end);
overflow: hidden;
white-space: nowrap;
}性能优化
流畅的动画需要达到每秒 60 帧 (fps),这意味着每帧必须在 16.67 毫秒内完成。未能满足此预算会导致可见的卡顿——画面抖动、丢帧和用户体验下降。理解浏览器渲染管线是编写高性能动画的关键。
渲染管线
当 CSS 属性变化时,浏览器最多经过三个阶段来更新显示:
- 样式:确定哪些 CSS 规则应用于哪些元素。
- 布局:计算每个元素的大小和位置。由 width、height、margin、padding、top、left、font-size 和其他几何属性的变化触发。
- 绘制:为每个元素填充像素。由 color、background、box-shadow、border-radius 和其他视觉属性的变化触发。
- 合成:按正确顺序组合已绘制的层。只有这一步在 GPU 上运行。由 transform 和 opacity 的变化触发。
只触发合成步骤的动画性能最佳,因为它们完全跳过了布局和绘制。这就是为什么 transform 和 opacity 是高性能 CSS 动画的黄金标准。
仅合成器属性
| 属性 | 触发布局? | 触发绘制? | GPU 合成? |
|---|---|---|---|
| transform | 否 | 否 | 是 |
| opacity | 否 | 否 | 是 |
| filter | 否 | 是 | 有时 |
| top / left | 是 | 是 | 否 |
| width / height | 是 | 是 | 否 |
| margin / padding | 是 | 是 | 否 |
| background-color | 否 | 是 | 否 |
| box-shadow | 否 | 是 | 否 |
使用 will-change
will-change 属性告诉浏览器哪些属性预期会变化,允许它通过将元素提升到自己的合成器层来提前优化。然而,它应该谨慎使用:
/* Apply will-change just before the animation */
.element:hover {
will-change: transform;
}
/* Remove it after the animation completes */
.element {
transition: transform 0.3s ease;
}will-change 的常见错误包括将其应用于太多元素(每个合成器层消耗 GPU 内存)和永久保留它(浪费资源)。好的做法是在动画开始前通过 JavaScript 添加 will-change,在动画结束后移除它。
使用 Transform 替代布局属性
最有效的性能优化是用 transform 替代触发布局的属性:
- 使用 transform: translateX(100px) 替代 left: 100px
- 使用 transform: translateY(50px) 替代 top: 50px
- 使用 transform: scale(1.5) 替代 width: 150%; height: 150%
- 使用 transform: rotate(45deg) 替代通过布局旋转
常见动画模式
某些动画模式在 Web 界面中反复出现。掌握这些模式为你提供了大多数常见 UI 动画需求的工具包。
1. 淡入
最简单且最通用的入场动画。元素从透明过渡到不透明:
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.4s ease-out forwards;
}2. 上滑淡入
一种更动态的入场效果,结合垂直移动和透明度变化。这创造了元素从下方到达的感觉:
@keyframes slideUpFade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-up {
animation: slideUpFade 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}3. 悬停缩放
悬停时微妙的缩放增加提供了令人满意的交互反馈:
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
}4. 交错列表动画
当项目列表出现时,为每个项目设置交错的动画延迟会创建引导视线的级联效果:
.list-item {
opacity: 0;
transform: translateY(10px);
animation: slideUpFade 0.3s ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0.0s; }
.list-item:nth-child(2) { animation-delay: 0.05s; }
.list-item:nth-child(3) { animation-delay: 0.1s; }
.list-item:nth-child(4) { animation-delay: 0.15s; }
.list-item:nth-child(5) { animation-delay: 0.2s; }对于动态列表,使用 JavaScript 根据每个项目的索引设置 animation-delay,或使用 CSS 自定义属性:style="animation-delay: calc(var(--i) * 0.05s)"。
5. 加载旋转器
平滑的连续旋转是经典的加载指示器:
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}动画无障碍:prefers-reduced-motion
并非所有用户都受益于动画。对于患有前庭障碍、运动敏感或某些认知状况的人,动画可能导致头晕、恶心或癫痫发作。prefers-reduced-motion 媒体查询允许你尊重用户系统级别的减少动画偏好。
实现减少动画
推荐的方法是让动画对未设置减少动画偏好的用户启用:
/* Default: no animation for users who prefer reduced motion */
.element {
opacity: 1;
transform: none;
}
/* Only animate for users who have no motion preference */
@media (prefers-reduced-motion: no-preference) {
.element {
animation: slideUpFade 0.5s ease-out forwards;
}
}或者,你可以为偏好减少动画的用户禁用特定动画:
.element {
animation: slideUpFade 0.5s ease-out forwards;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: none;
opacity: 1;
transform: none;
}
/* Keep functional transitions but shorten duration */
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}保留什么和移除什么
当用户偏好减少动画时,并非所有动画都应该被移除。传达含义或状态的功能性动画应保留,只是简化:
- 移除:装饰性动画、视差滚动、自动播放轮播、脉冲效果和循环背景动画。
- 简化:过渡持续时间应缩短到接近瞬时(0.01ms)而非完全移除,以便状态变化仍然可感知。
- 保留:功能性状态过渡(悬停、聚焦、活动状态)、进度指示器和任何传达重要信息的动画。
动画库
虽然 CSS 动画处理大多数 UI 需求,JavaScript 动画库提供了高级功能,如基于物理的运动、滚动关联动画和复杂序列的编排。
CSS 优先的库
- Animate.css:预构建的 CSS 动画类集合。只需向元素添加类名即可应用动画。适合快速原型设计,但如果过度使用可能导致千篇一律的动效。
- Hamburgers:动画汉堡菜单图标过渡集合,全部用纯 CSS 实现。
JavaScript 动画库
- Framer Motion:React 动画库,提供声明式动画 API,支持弹簧物理、布局动画和手势支持。React 应用的最佳选择。
- GSAP (GreenSock):最强大的 JavaScript 动画库。提供精确的时间线控制、复杂缓动、SVG 变形和滚动触发动画。被大品牌用于营销网站和互动体验。
- Anime.js:轻量级 JavaScript 动画库,具有简洁的 API。支持 CSS 属性、SVG、DOM 属性和 JavaScript 对象。CSS 和 GSAP 之间的良好中间选择。
- Motion One:基于 Web Animations API 构建的现代轻量动画库。提供最小化 API,支持弹簧物理和时间线。
何时使用 JavaScript vs CSS
CSS 用于响应状态变化的简单声明式动画。JavaScript 用于:基于物理的运动(弹簧、动量)、滚动关联动画、具有精确时间控制的复杂序列、运行时计算的动态动画参数,或需要以编程方式暂停、反转或拖拽的动画。
调试动画
动画 bug 可能很微妙且难以诊断。正确的调试工具和技术使过程更加高效。
Chrome DevTools 动画面板
Chrome DevTools 包含专门的动画面板,提供强大的调试功能:
- 时间线视图:查看页面上所有活动动画及其持续时间、延迟和关键帧。
- 播放控制:将动画减速到 1/4 速度、暂停或逐帧拖动时间线。
- 属性检查:点击时间线中的动画查看被动画的确切 CSS 属性及其在每个关键帧的值。
- 实时编辑:无需重新加载页面即可实时修改动画持续时间、延迟和缓动。
性能分析
使用 Chrome DevTools 中的 Performance 面板识别动画卡顿:
- 在动画运行时录制性能追踪
- 查找超过 16.67ms 预算的帧(在帧图表中显示为红色条)
- 检查动画帧中是否有「Layout」操作,这表示触发布局的属性
- 使用「Rendering」选项卡启用「Paint flashing」(以绿色显示重绘区域)和「Layer borders」(显示合成器层)
常见调试场景
- 动画不运行:检查 animation-fill-mode 是否设置正确。没有 forwards 或 both,元素会回到动画前状态。还要验证元素具有正确的尺寸——transform: scale(0) 使元素不可见但仍存在。
- 动画卡顿:打开 Performance 面板查找长帧。检查是否在动画触发布局的属性。切换到 transform 和 opacity。
- 动画延迟感觉不对:记住 animation-delay 在动画开始前创建间隔。在此间隔期间,除非设置 animation-fill-mode: backwards 或 both,否则元素显示动画前状态。
- 动画在 Chrome 中有效但在 Safari 中无效:Safari 对 animation 简写的解析更严格。确保所有值顺序正确且未使用不支持的特性。
常见问题
应该使用 CSS 过渡还是 CSS 动画?
CSS 过渡用于从 A 点到 B 点的简单状态变化,如悬停效果、焦点状态和切换可见性。CSS 动画(关键帧)用于复杂的多步序列、循环动画或需要独立于状态变化运行的动画。过渡更简单;动画提供更多控制。
哪些 CSS 属性可以安全地做动画以保证性能?
动画性能最好的属性是 transform 和 opacity,因为它们可以完全由 GPU 合成器处理,无需触发布局或绘制。width、height、margin 和 top 等其他属性会触发布局重计算,开销很大。始终优先使用 transform: translateX() 而非 left,使用 transform: scale() 而非 width/height 变化。
如何为对动画敏感的用户提供无障碍支持?
使用 prefers-reduced-motion 媒体查询为在操作系统中启用「减少动画」设置的用户禁用或简化动画。将非必要动画包裹在 @media (prefers-reduced-motion: no-preference) 中,并为偏好减少动画的用户提供即时状态变化作为后备。
什么是 will-change,什么时候应该使用?
will-change 是一个 CSS 属性,向浏览器提示将来哪些属性会变化,允许它提前优化。谨慎使用,仅在观察到性能问题时才使用。在动画开始前应用,动画结束后移除。过度使用 will-change 会浪费 GPU 内存,实际上可能损害性能。
如何调试 CSS 动画?
Chrome DevTools 提供了强大的动画调试工具。打开动画面板(在更多工具菜单中)查看所有活动动画、拖动时间线、实时修改持续时间和延迟、将动画减速到 1/4 速度。你还可以使用 Performance 面板识别动画卡顿和布局抖动。