CSS 动画与过渡

系统掌握 CSS transition 和 animation 的完整属性、时序函数、@keyframes 语法、animation-fill-mode 的作用时机,以及 transform 和 opacity 的性能优化特性。

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 属性适合做动画?哪些不适合?为什么?

回答要点transformopacity 性能最好,只触发 Composite 阶段。不适合动画的属性包括 width/height(触发 Layout)、background-color(触发 Paint)、display(不可过渡)。动画性能优化的核心是避免触发 Layout 和 Paint。

Q: will-change 应该怎么用?有什么副作用?

回答要点will-change 提示浏览器提前为特定属性创建优化上下文。正确用法是只对需要动画的属性使用、在动画开始前声明、在动画结束后清理。副作用包括增加内存消耗、可能导致层叠上下文变化影响 z-index。


延展阅读