CSS动画与过渡

What — 是什么

CSS Transition 用于状态间的平滑过渡,CSS Animation 用于定义关键帧动画。两者都是声明式、GPU 加速的高性能动画方案。

transition 完整属性

transition 是以下四个属性的简写:

属性说明默认值示例
transition-property指定过渡的 CSS 属性alltransform, opacity, background-color
transition-duration过渡持续时间0s0.3s, 200ms
transition-timing-function时间函数(速度曲线)easelinear, cubic-bezier(0.4,0,0.2,1)
transition-delay延迟时间0s0.1s, 100ms
/* 简写 */
transition: property duration timing-function delay;

/* 示例 */
transition: transform 0.3s ease 0.1s;

/* 多属性过渡 */
transition: transform 0.3s ease, opacity 0.3s ease, box-shadow 0.2s ease-in;

/* 分写 */
transition-property: transform, opacity;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0.1s;

注意: transition-property: all 虽然方便,但会让浏览器对所有可动画属性做过渡准备,影响性能。建议明确指定需要过渡的属性。

timing-function 详解

时间函数决定动画在持续时间内的速度变化曲线:

函数曲线描述典型场景
linear匀速,无加减速进度条、匀速旋转
ease慢→快→慢(默认)通用过渡
ease-in慢→快(加速)元素退出、下落
ease-out快→慢(减速)元素进入、弹窗出现
ease-in-out慢→快→慢(对称)颜色变化、淡入淡出
cubic-bezier(x1,y1,x2,y2)自定义贝塞尔曲线弹性效果、特殊曲线
steps(n, <jumpterm>)逐帧离散跳变打字效果、精灵图动画
/* 标准缓动 */
ease:            cubic-bezier(0.25, 0.1, 0.25, 1.0)
ease-in:         cubic-bezier(0.42, 0.0, 1.0, 1.0)
ease-out:        cubic-bezier(0.0, 0.0, 0.58, 1.0)
ease-in-out:     cubic-bezier(0.42, 0.0, 0.58, 1.0)

/* Material Design 推荐曲线 */
standard:        cubic-bezier(0.2, 0.0, 0, 1.0)   /* 通用 */
decelerate:      cubic-bezier(0.0, 0.0, 0, 1.0)   /* 进入 */
accelerate:      cubic-bezier(0.3, 0.0, 1, 1.0)   /* 退出 */

/* 弹性曲线 */
spring:          cubic-bezier(0.34, 1.56, 0.64, 1) /* 轻微过冲回弹 */
bounce-in:       cubic-bezier(0.6, -0.28, 0.74, 0.05) /* 大幅过冲 */

steps() 参数详解:

/* steps(number_of_steps, direction) */
/* direction: jump-start / jump-end / jump-none / jump-both */
/* jump-end(原 start): 间隔末尾跳变,默认 */
/* jump-start(原 end): 间隔开头跳变 */

/* 打字效果:10步,末尾跳变 */
animation: typing 1s steps(10, jump-end) forwards;

/* 精灵图:8帧,开头跳变 */
animation: sprite 0.8s steps(8, jump-start) infinite;

animation 完整属性

animation 是以下八个属性的简写:

属性说明默认值示例
animation-name@keyframes 名称nonefadeIn, spin
animation-duration动画持续时间0s0.5s, 1s
animation-timing-function时间函数easelinear, cubic-bezier(...)
animation-delay延迟时间0s0.2s
animation-iteration-count播放次数13, infinite
animation-direction播放方向normalreverse, alternate, alternate-reverse
animation-fill-mode填充模式noneforwards, backwards, both
animation-play-state播放状态runningpaused
/* 简写 */
animation: name duration timing-function delay iteration-count direction fill-mode play-state;

/* 示例 */
animation: fadeInUp 0.4s ease-out 0.1s 1 normal forwards running;

/* 常用简写(省略使用默认值) */
animation: spin 1s linear infinite;          /* 旋转 */
animation: pulse 2s ease-in-out alternate;   /* 来回脉冲 */
animation: slideIn 0.3s ease-out forwards;   /* 入场保持 */

animation-direction 取值:

说明
normal每次从头到尾播放
reverse每次从尾到头播放
alternate正→反→正→反交替
alternate-reverse反→正→反→正交替

animation-fill-mode 取值:

说明
none默认,动画结束后回到原始样式
forwards动画结束后保持最后一帧状态
backwards动画延迟期间应用第一帧状态
both同时应用 forwards 和 backwards 行为

@keyframes 高级用法

/* 基础:from/to */
@keyframes fadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* 多关键帧百分比 */
@keyframes bounce {
    0%   { transform: translateY(0); }
    20%  { transform: translateY(-30px); }
    40%  { transform: translateY(0); }
    60%  { transform: translateY(-15px); }
    80%  { transform: translateY(0); }
    100% { transform: translateY(0); }
}

/* 同一关键帧多个属性 */
@keyframes complexEntry {
    0% {
        opacity: 0;
        transform: translateY(40px) scale(0.9);
        filter: blur(4px);
    }
    60% {
        opacity: 1;
        transform: translateY(-5px) scale(1.02);
        filter: blur(0);
    }
    100% {
        opacity: 1;
        transform: translateY(0) scale(1);
        filter: blur(0);
    }
}

/* 多选择器复用同一关键帧 */
@keyframes slideIn {
    from { transform: translateX(-100%); opacity: 0; }
    to   { transform: translateX(0); opacity: 1; }
}

.item-1 { animation: slideIn 0.4s ease-out 0.0s both; }
.item-2 { animation: slideIn 0.4s ease-out 0.1s both; }
.item-3 { animation: slideIn 0.4s ease-out 0.2s both; }

transform 完整函数

函数说明示例
translate(x, y)2D 平移translate(10px, 20px)
translateX(x)水平平移translateX(50px)
translateY(y)垂直平移translateY(-10px)
translate3d(x, y, z)3D 平移translate3d(0, 0, 100px)
translateZ(z)Z 轴平移translateZ(50px)
scale(x, y)2D 缩放scale(1.5, 1.2)
scaleX(x)水平缩放scaleX(2)
scaleY(y)垂直缩放scaleY(0.5)
scale3d(x, y, z)3D 缩放scale3d(1, 1, 1.5)
rotate(angle)2D 旋转rotate(45deg)
rotateX(angle)绕 X 轴旋转rotateX(60deg)
rotateY(angle)绕 Y 轴旋转rotateY(45deg)
rotateZ(angle)绕 Z 轴旋转rotateZ(90deg)
rotate3d(x, y, z, angle)3D 旋转rotate3d(1, 1, 0, 45deg)
skew(x-angle, y-angle)2D 倾斜skew(10deg, 5deg)
skewX(angle)水平倾斜skewX(15deg)
skewY(angle)垂直倾斜skewY(10deg)
matrix(a,b,c,d,e,f)2D 矩阵变换matrix(1,0,0,1,10,20)
matrix3d(...)3D 矩阵变换(16值)高级用法
perspective(n)设置透视距离perspective(500px)
/* transform 组合:从右往左依次应用 */
.transform-example {
    /* 先平移 → 再旋转 → 再缩放 */
    transform: translate(50px, 20px) rotate(30deg) scale(1.2);
}

/* transform-origin:变换原点 */
.rotate-center  { transform-origin: center center; transform: rotate(45deg); }  /* 默认 */
.rotate-top-left { transform-origin: top left; transform: rotate(45deg); }
.rotate-custom   { transform-origin: 30% 70%; transform: rotate(45deg); }

/* perspective:透视(在父元素上设置) */
.perspective-container {
    perspective: 800px;       /* 透视距离,值越小透视效果越强 */
    perspective-origin: 50% 50%; /* 视点位置 */
}

/* transform-style:保留 3D 空间 */
.preserve-3d {
    transform-style: preserve-3d;  /* 子元素保持 3D 变换 */
    /* flat(默认):子元素扁平化到 2D 平面 */
}

/* backface-visibility:背面可见性 */
.card {
    backface-visibility: hidden; /* 隐藏翻转后的背面 */
}

性能优化原理:GPU 合成层

浏览器渲染管线:

JavaScript → Style → Layout → Paint → Composite
                        ↑        ↑        ↑
                     重排      重绘     合成
                   (慢)      (中)     (快/GPU)
操作触发阶段性能影响
修改 width/height/marginLayout → Paint → Composite差(重排)
修改 color/background/box-shadowPaint → Composite中(重绘)
修改 transform/opacityComposite好(仅合成)

will-change 的作用:

/* 提前告知浏览器该属性会变化,让浏览器优化准备 */
.will-animate {
    will-change: transform, opacity;
}

/* ⚠️ 使用原则:不要滥用! */
/* ❌ 错误:全局设置,浪费内存 */
* { will-change: transform; }

/* ✅ 正确:在需要时添加,动画结束后移除 */
/* 方式1:hover 时设置(此时浏览器有足够时间准备) */
.card { transition: transform 0.3s; }
.card:hover { will-change: transform; }

/* 方式2:JS 动态添加和移除 */
// element.style.willChange = 'transform';
// 动画结束后:element.style.willChange = 'auto';

触发合成层的条件:

  • 3D 变换:translate3dtranslateZ
  • will-change: transform / will-change: opacity
  • opacity 动画(值 < 1)
  • position: fixed(部分浏览器)
  • filterbackdrop-filter
  • videocanvasiframe 元素

注意: 每个合成层都会占用额外内存(约 width x height x 4 bytes),过度创建会导致内存暴涨。

CSS 动画 vs JS 动画 vs Web Animations API

维度CSS 动画JS 动画(requestAnimationFrame/GSAP)Web Animations API
性能极高(GPU 加速)高(需手动优化)高(CSS 动画的 JS 接口)
控制力低(声明式)极高(可暂停/反转/seek)高(可暂停/反转/seek)
复杂度简单
兼容性极好极好好(现代浏览器)
状态同步难(无法读取中间状态)
动态参数不支持支持支持
编排链困难简单(Timeline)中等

Web Animations API 示例:

// 用 JS 调用 CSS 级别的动画能力
const animation = element.animate(
    [
        { transform: 'translateY(20px)', opacity: 0 },
        { transform: 'translateY(0)', opacity: 1 }
    ],
    {
        duration: 300,
        easing: 'ease-out',
        fill: 'forwards'
    }
);

// 完整控制
animation.pause();          // 暂停
animation.play();           // 恢复
animation.reverse();        // 反转
animation.finish();         // 跳到结束
animation.currentTime = 150; // seek 到中间
animation.onfinish = () => {}; // 完成回调

View Transitions API 概念

View Transitions API 是浏览器原生的页面/元素过渡方案,可以让 DOM 变化时有平滑的视觉过渡。

/* 定义过渡动画 */
::view-transition-old(root) {
    animation: fade-out 0.25s ease-out;
}
::view-transition-new(root) {
    animation: fade-in 0.25s ease-in;
}

/* 自定义元素过渡 */
::view-transition-old(card) {
    animation: slide-out 0.3s ease-in;
}
::view-transition-new(card) {
    animation: slide-in 0.3s ease-out;
}
// 基本用法
document.startViewTransition(() => {
    // 在这里更新 DOM
    updateDOM();
});

// SPA 路由切换
async function navigate(url) {
    const transition = document.startViewTransition(async () => {
        await loadPage(url);
        updateContent();
    });
    await transition.finished;
}

浏览器支持: Chrome 111+、Edge 111+,Safari 和 Firefox 逐步跟进。

Why — 为什么

适用场景

  • 按钮悬浮/点击反馈(transition)
  • 加载动画(animation + keyframes)
  • 页面过渡和元素入场
  • 交互微动效
  • 数据可视化动画
  • 3D 卡片翻转效果

CSS 动画 vs JS 动画对比

对比维度CSS 动画JS 动画
渲染性能高(浏览器自动优化,可跳帧)需手动优化(rAF 节奏控制)
主线程不阻塞(合成层动画)可能阻塞(在主线程执行)
控制能力弱(声明式,无法暂停/seek/反转)强(可精确控制每个参数)
动态参数不支持(需预定义)支持(运行时计算)
动画链编排困难(animation-delay hack)简单(GSAP Timeline、Promise)
代码量少(声明式)多(命令式)
调试DevTools Animations 面板断点 / console.log
兼容性极好极好
减少动画prefers-reduced-motion 原生支持需手动检测

选择建议:

  • 简单交互反馈(hover/click) → CSS transition
  • 循环/入场/退场动画 → CSS animation
  • 复杂编排/需要控制 → JS 动画(GSAP/WAAPI)
  • 路由切换过渡 → View Transitions API

什么时候用 transition vs animation

场景推荐方案原因
hover/click 状态切换transition两态切换,简洁
页面加载入场animation自动播放,可设 fill-mode
循环旋转/脉冲animationiteration-count: infinite
折叠/展开transition状态驱动,两态
多步复杂路径animation多关键帧
交错入场animation + delay多元素编排
用户拖拽跟随JS + transition需动态参数

性能对比

属性动画性能排序(从好到差):

1. transform / opacity     → 仅 Composite(GPU 加速)
2. filter / backdrop-filter → Composite + 部分 Paint
3. color / background      → Paint + Composite
4. width / height / margin → Layout + Paint + Composite(重排)
5. top / left              → Layout + Paint + Composite(重排)

GPU 加速原理:

  • 浏览器将元素提升到独立的合成层(Compositing Layer)
  • 合成层由 GPU 直接处理,不经过 CPU 的 Layout 和 Paint 阶段
  • 动画时只需在 GPU 上做矩阵运算,极快
  • 但每个合成层都要占内存,不能滥用

How — 怎么用

快速上手

/* 过渡:hover 效果 */
.button {
    background: #3b82f6;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.button:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

/* 关键帧动画:加载旋转 */
@keyframes spin {
    to { transform: rotate(360deg); }
}
.spinner {
    animation: spin 1s linear infinite;
}

示例1:常用过渡效果

/* ---- 颜色过渡 ---- */
.color-btn {
    background-color: #3b82f6;
    color: white;
    transition: background-color 0.3s ease, color 0.3s ease;
}
.color-btn:hover {
    background-color: #1d4ed8;
    color: #e0e7ff;
}

/* ---- 尺寸过渡(用 transform 替代 width/height) ---- */
.scale-card {
    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.scale-card:hover {
    transform: scale(1.05);
}

/* ---- 位置过渡 ---- */
.slide-box {
    transition: transform 0.4s ease-out;
}
.slide-box.active {
    transform: translateX(200px);
}

/* ---- 变形过渡(旋转+缩放组合) ---- */
.icon-btn {
    transition: transform 0.2s ease;
}
.icon-btn:hover {
    transform: rotate(15deg) scale(1.1);
}

/* ---- 多属性组合过渡 ---- */
.fancy-card {
    background: white;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    transition: transform 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
.fancy-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 24px rgba(0,0,0,0.15);
    background: #f0f9ff;
}

示例2:贝塞尔曲线调试与自定义

/* Material Design 标准缓动曲线 */
.md-standard {
    transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
}

/* iOS 弹性曲线 */
.ios-spring {
    transition: transform 0.5s cubic-bezier(0.28, 0.11, 0.32, 1);
    /* 或更强的弹跳 */
    transition: transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

/* 过冲回弹(值超过1会产生过冲效果) */
.overshoot {
    /* Y值 > 1 会让动画超过目标值再回来 */
    transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* 快出慢停 */
.quick-out {
    transition: transform 0.3s cubic-bezier(0.0, 0.0, 0.2, 1);
}

/* 在线调试工具:cubic-bezier.com */
/* Chrome DevTools:点击时间函数可直接拖拽调参 */

示例3:steps() 逐帧动画

/* ---- 打字效果 ---- */
@keyframes typing {
    from { width: 0; }
    to   { width: 20ch; }
}

.typing-text {
    font-family: monospace;
    white-space: nowrap;
    overflow: hidden;
    border-right: 2px solid currentColor;
    width: 0;
    animation: typing 2s steps(20) forwards,
               blink 0.5s step-end infinite alternate;
}

@keyframes blink {
    50% { border-color: transparent; }
}

/* ---- 精灵图动画 ---- */
/*
 * 假设精灵图:8帧,横向排列
 * 每帧 64x64px,总宽 512px
 */
@keyframes walk {
    from { background-position: 0 0; }
    to   { background-position: -512px 0; }
}

.sprite-character {
    width: 64px;
    height: 64px;
    background-image: url('sprite-walk.png');
    animation: walk 0.8s steps(8) infinite;
}

/* ---- 进度条分段 ---- */
@keyframes progress {
    from { width: 0; }
    to   { width: 100%; }
}

.step-progress {
    animation: progress 4s steps(10) forwards;
    /* 10段,每段10%,离散跳变 */
}

示例4:animation-fill-mode 详解

/* ---- fill-mode: none(默认) ---- */
/* 动画结束后回到原始样式,延迟期间也是原始样式 */
.demo-none {
    opacity: 1;
    animation: fadeOut 1s ease 0.5s none;
    /* 0-0.5s: opacity:1(原始样式)
       0.5-1.5s: 动画中
       1.5s后: opacity:1(回到原始样式) */
}

/* ---- fill-mode: forwards ---- */
/* 动画结束后保持最后一帧状态 */
.demo-forwards {
    opacity: 0;
    animation: fadeIn 1s ease 0.5s forwards;
    /* 0-0.5s: opacity:0(原始样式)
       0.5-1.5s: 动画中
       1.5s后: opacity:1(保持最后一帧) */
}

/* ---- fill-mode: backwards ---- */
/* 延迟期间应用第一帧状态,结束后回到原始样式 */
.demo-backwards {
    opacity: 1;
    animation: fadeOut 1s ease 0.5s backwards;
    /* 0-0.5s: opacity:0(第一帧状态覆盖)
       0.5-1.5s: 动画中
       1.5s后: opacity:1(回到原始样式) */
}

/* ---- fill-mode: both ---- */
/* 延迟期间应用第一帧,结束后保持最后一帧 */
.demo-both {
    opacity: 0;
    animation: fadeIn 1s ease 0.5s both;
    /* 0-0.5s: opacity:0(第一帧)
       0.5-1.5s: 动画中
       1.5s后: opacity:1(最后一帧) */
}

@keyframes fadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes fadeOut {
    from { opacity: 1; }
    to   { opacity: 0; }
}

示例5:复杂关键帧动画

/* ---- 弹跳小球 ---- */
@keyframes bounce {
    0%, 100% {
        transform: translateY(0);
        animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
    }
    50% {
        transform: translateY(-80px);
        animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
    }
}
.bouncing-ball {
    animation: bounce 1s infinite;
}

/* ---- 脉冲光环 ---- */
@keyframes pulse-ring {
    0% {
        transform: scale(0.8);
        opacity: 1;
    }
    100% {
        transform: scale(2.2);
        opacity: 0;
    }
}
.pulse-dot {
    position: relative;
}
.pulse-dot::before {
    content: '';
    position: absolute;
    inset: -4px;
    border-radius: 50%;
    border: 2px solid currentColor;
    animation: pulse-ring 1.5s ease-out infinite;
}

/* ---- 摇晃提示(输入错误) ---- */
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    10%, 50%, 90% { transform: translateX(-4px); }
    30%, 70% { transform: translateX(4px); }
}
.shake {
    animation: shake 0.5s ease-in-out;
}

/* ---- 数字翻转(计数器效果) ---- */
@keyframes countUp {
    from { transform: translateY(100%); opacity: 0; }
    to   { transform: translateY(0); opacity: 1; }
}
.counter-digit {
    display: inline-block;
    overflow: hidden;
}
.counter-digit span {
    display: block;
    animation: countUp 0.4s ease-out both;
}

/* ---- 交错入场(配合 JS 设置 delay) ---- */
.stagger-item {
    opacity: 0;
    transform: translateY(20px);
    animation: fadeInUp 0.5s ease-out both;
    /* JS: element.style.animationDelay = `${index * 0.05}s` */
}

示例6:3D 变换与透视

/* ---- 3D 卡片翻转 ---- */
.card-container {
    perspective: 800px;
}
.card-inner {
    position: relative;
    width: 300px;
    height: 200px;
    transform-style: preserve-3d;
    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-container:hover .card-inner {
    transform: rotateY(180deg);
}
.card-front, .card-back {
    position: absolute;
    inset: 0;
    backface-visibility: hidden;
    border-radius: 12px;
}
.card-back {
    transform: rotateY(180deg);
}

/* ---- 3D 倾斜(鼠标跟踪) ---- */
.tilt-card {
    transform-style: preserve-3d;
    transition: transform 0.1s ease-out;
    /* JS 配合:
       card.style.transform =
         `rotateY(${mouseX}deg) rotateX(${mouseY}deg)` */
}

/* ---- 3D 立方体 ---- */
.cube-wrapper {
    perspective: 600px;
}
.cube {
    width: 100px;
    height: 100px;
    transform-style: preserve-3d;
    animation: cubeRotate 6s linear infinite;
}
.cube-face {
    position: absolute;
    width: 100px;
    height: 100px;
}
.cube-face.front  { transform: translateZ(50px); }
.cube-face.back   { transform: translateZ(-50px) rotateY(180deg); }
.cube-face.left   { transform: translateX(-50px) rotateY(-90deg); }
.cube-face.right  { transform: translateX(50px) rotateY(90deg); }
.cube-face.top    { transform: translateY(-50px) rotateX(90deg); }
.cube-face.bottom { transform: translateY(50px) rotateX(-90deg); }

@keyframes cubeRotate {
    0%   { transform: rotateX(0) rotateY(0); }
    100% { transform: rotateX(360deg) rotateY(360deg); }
}

/* ---- 视差滚动效果 ---- */
.parallax-container {
    perspective: 1px;
    height: 100vh;
    overflow-x: hidden;
    overflow-y: auto;
}
.parallax-layer-back {
    transform: translateZ(-2px) scale(3);
}
.parallax-layer-front {
    transform: translateZ(0);
}

示例7:动画性能优化实战

/* ---- 1. 只动画 transform 和 opacity ---- */

/* ❌ 差:触发 Layout */
.bad-animate {
    transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}

/* ✅ 好:只触发 Composite */
.good-animate {
    transition: transform 0.3s, opacity 0.3s;
    /* 用 scaleX/Y 代替 width/height */
    /* 用 translate 代替 top/left */
}

/* ---- 2. contain 属性隔离布局 ---- */
.isolated-component {
    contain: layout style paint;
    /* 限制重排范围,避免影响其他元素 */
}

/* ---- 3. content-visibility 延迟渲染 ---- */
.lazy-section {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px;
    /* 不在视口内时跳过渲染 */
}

/* ---- 4. will-change 谨慎使用 ---- */
/* ✅ hover 时添加 */
.card {
    transition: transform 0.3s ease;
}
.card:hover {
    will-change: transform;
}

/* ✅ 入场动画前添加,结束后移除(JS) */
// element.style.willChange = 'transform, opacity';
// requestAnimationFrame(() => {
//     element.classList.add('animate');
//     element.addEventListener('animationend', () => {
//         element.style.willChange = 'auto';
//     }, { once: true });
// });

/* ---- 5. 避免同时动画大量元素 ---- */
/* ❌ 100个元素同时入场 */
.list-item {
    animation: fadeIn 0.3s ease-out both;
}

/* ✅ 分批入场,减少同时合成层数量 */
.list-item {
    opacity: 0;
    animation: fadeIn 0.3s ease-out both;
    animation-delay: calc(var(--i) * 30ms);
    /* 或使用 IntersectionObserver 分批触发 */
}

/* ---- 6. 动画结束后移除合成层 ---- */
.animated-element {
    animation: slideIn 0.3s ease-out both;
}
/* JS: 动画结束后可以移除 animation,
   让浏览器回收合成层内存 */

/* ---- 7. 减少阴影和滤镜在动画中的使用 ---- */
/* ❌ 阴影变化触发 Paint */
.bad-shadow { transition: box-shadow 0.3s; }

/* ✅ 用伪元素 + opacity 模拟阴影 */
.good-shadow {
    position: relative;
}
.good-shadow::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    box-shadow: 0 8px 24px rgba(0,0,0,0.2);
    opacity: 0;
    transition: opacity 0.3s;
}
.good-shadow:hover::after {
    opacity: 1;
}

减少动画偏好适配

/* ---- 基础适配 ---- */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ---- 精细适配:保留必要反馈 ---- */
@media (prefers-reduced-motion: reduce) {
    /* 移除装饰性动画 */
    .decoration,
    .background-animation {
        animation: none !important;
    }

    /* 保留交互反馈,但缩短时间 */
    .button,
    .link {
        transition-duration: 0.01ms !important;
    }

    /* 入场改为直接显示 */
    .fade-in-up {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
    }
}

/* ---- JS 检测 ---- */
// const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
// if (prefersReducedMotion.matches) {
//     // 跳过动画或使用极简方案
// }
// prefersReducedMotion.addEventListener('change', (e) => {
//     // 动态切换
// });

常见问题与踩坑

问题原因解决方案
动画卡顿动画了 layout 属性(width/height/margin)只动画 transformopacity
动画结束状态丢失默认 fill-mode: noneanimation-fill-mode: forwardsboth
height: auto 无法过渡auto 不是数值,无法插值max-height hack 或 grid-template-rows: 0fr → 1fr
移动端动画闪烁层合成问题transform: translateZ(0)will-change: transform
transition 不生效display: none → block 不可过渡visibility + opacity 或 JS 两帧延迟
动画闪烁/抖动亚像素渲染问题transform: translateZ(0)backface-visibility: hidden
transform 覆盖问题多次赋值 transform 会覆盖合并到一条 transform 声明中
animation-delay 占用周期delay 不算在 iteration 内注意 iteration-count 不包含 delay 时间
border-radius + overflow: hidden + 动画合成层裁剪异常will-change: transform 或用 clip-path 替代
transition 自动反向只在属性变化时过渡正常行为:hover 进入和离开都会触发过渡
animation 不自动反向direction 默认 normalalternate 实现来回动画
多动画冲突同一属性被多个 animation 操作避免多个动画操作同一属性

最佳实践

  • 只动画 transformopacity
  • will-change 提示浏览器,用完移除
  • 始终添加 prefers-reduced-motion 降级
  • 交互反馈用 transition(hover/click),循环动效用 animation
  • animation-fill-mode: both 是最安全的默认选择
  • 大量元素动画时分批进行,减少同时合成层
  • containcontent-visibility 限制重排范围
  • 复杂动画优先考虑 GSAP 或 Web Animations API
  • 阴影过渡用伪元素 + opacity 模拟,避免 Paint
  • 用 CSS 自定义属性(--delay)实现交错动画

面试题

Q1: CSS transition 和 animation 的区别是什么?

transition 用于两个状态间的平滑过渡,需要触发条件(如 hover),只能定义开始和结束状态,执行一次后停止;animation 通过 @keyframes 定义关键帧序列,可自动播放、循环、暂停,支持多个关键帧和复杂动画路径,不需要触发条件。交互反馈用 transition,循环/入场动效用 animation。

Q2: 为什么推荐用 transform 和 opacity 做动画?

transform 和 opacity 的动画只触发合成层(Composite)操作,由 GPU 加速处理,跳过 CPU 的 Layout 和 Paint 阶段,性能最佳。动画 width/height/margin/top/left 等属性会触发布局重排(Layout + Paint + Composite),导致主线程阻塞造成卡顿。

Q3: will-change 的作用和使用原则?

will-change 提前告知浏览器哪些属性即将变化,让浏览器可以提前创建合成层、优化渲染管线,减少动画启动时的卡顿。使用原则:(1) 不要全局滥用,每个合成层都占额外内存;(2) 在需要时添加,动画结束后移除;(3) 优先在 hover/focus 时设置,给浏览器准备时间;(4) 如果页面已经很流畅,不需要加 will-change。

Q4: 如何实现动画的暂停和恢复?

CSS 方案:通过 animation-play-state: paused / running 控制,可配合 JS 切换类名。Web Animations API 方案:调用 animation.pause()animation.play() 方法,还支持 reverse()finish()seek(currentTime) 等更精细的控制。WAAPI 方案功能更强,推荐在需要动态控制时使用。

Q5: 什么是GPU加速(合成层)?如何触发?

GPU 加速是指浏览器将元素提升到独立的合成层,由 GPU 直接处理渲染,跳过 CPU 布局和绘制。触发方式:3D 变换(translateZ/translate3d)、will-change: transform/opacity、opacity 动画(值 < 1)、position: fixed(部分浏览器)、filter、backdrop-filter。注意不要过度创建合成层,每个合成层约消耗 width x height x 4 bytes 内存。

Q6: height: auto 如何实现过渡动画?

auto 不是数值无法插值,直接 transition 无效。方案:(1) max-height hack:设一个足够大的 max-height 值,从 0 过渡到该值,但时间曲线不精确;(2) grid-template-rows 从 0fr 过渡到 1fr(现代方案,动画精确);(3) JS 读取实际高度后设置具体像素值过渡(最精确但需要 JS)。

Q7: animation-fill-mode 各值有什么区别?

none(默认):动画结束后回到原始样式;forwards:结束后保持最后一帧状态;backwards:延迟期间应用第一帧状态;both:同时应用 forwards 和 backwards 行为。入场动画推荐 both,确保延迟期间显示初始状态、结束后保持最终状态。

Q8: CSS 动画和 JS 动画如何选择?

简单的交互反馈(hover/click 状态切换)用 CSS transition;自动播放的循环/入场动画用 CSS animation;需要暂停/恢复/反转/动态参数等精细控制时用 Web Animations API 或 GSAP;复杂动画编排(Timeline 链式动画)用 GSAP。CSS 动画性能更好(GPU 加速、不阻塞主线程),JS 动画控制力更强,根据场景权衡选择。


相关链接: