ToolHub
View All Posts

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 属性控制动画运行前后应用哪些样式。这是最常被误解的动画属性之一:

缓动函数

缓动函数(也称为定时函数)定义动画的加速曲线。它们决定动画是缓慢开始然后加速、快速开始然后减速,还是遵循更复杂的模式。正确的缓动函数使动画感觉自然;错误的缓动函数使动画感觉机械或突兀。

内置缓动函数

三次贝塞尔曲线

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 属性变化时,浏览器最多经过三个阶段来更新显示:

  1. 样式:确定哪些 CSS 规则应用于哪些元素。
  2. 布局:计算每个元素的大小和位置。由 width、height、margin、padding、top、left、font-size 和其他几何属性的变化触发。
  3. 绘制:为每个元素填充像素。由 color、background、box-shadow、border-radius 和其他视觉属性的变化触发。
  4. 合成:按正确顺序组合已绘制的层。只有这一步在 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 或 opacity 实现相同的视觉效果,始终优先使用它们,而不是触发布局或绘制的属性。这一原则可以消除大多数动画性能问题。

常见动画模式

某些动画模式在 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;
  }
}

保留什么和移除什么

当用户偏好减少动画时,并非所有动画都应该被移除。传达含义或状态的功能性动画应保留,只是简化:

动画库

虽然 CSS 动画处理大多数 UI 需求,JavaScript 动画库提供了高级功能,如基于物理的运动、滚动关联动画和复杂序列的编排。

CSS 优先的库

JavaScript 动画库

何时使用 JavaScript vs CSS

CSS 用于响应状态变化的简单声明式动画。JavaScript 用于:基于物理的运动(弹簧、动量)、滚动关联动画、具有精确时间控制的复杂序列、运行时计算的动态动画参数,或需要以编程方式暂停、反转或拖拽的动画。

调试动画

动画 bug 可能很微妙且难以诊断。正确的调试工具和技术使过程更加高效。

Chrome DevTools 动画面板

Chrome DevTools 包含专门的动画面板,提供强大的调试功能:

性能分析

使用 Chrome DevTools 中的 Performance 面板识别动画卡顿:

常见调试场景

正在使用 CSS?试试我们免费的 CSS 工具来格式化、压缩和优化你的样式表以用于生产环境。

CSS 格式化工具CSS 压缩工具

常见问题

应该使用 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 面板识别动画卡顿和布局抖动。