React 动画实现方案:CSS、Framer Motion 与渲染性能

引言

动画是用户体验的「质感层」:恰到好处的过渡让状态变化可理解,克制的动效让界面显得精致。React 生态的动画方案五花八门——纯 CSS 过渡、CSS 动画、Web Animations API、Framer Motion、React Transition Group……本文将从「动画与渲染的关系」讲起,系统梳理 React 中动画的实现路径、状态驱动的动画模式、入场离场动画与性能优化要点。

一、动画的本质:变化的插值

无论用什么工具,动画的本质都是「在一段时间内平滑地改变某些属性」。关键问题在于:这个变化由谁计算、在哪条线程执行

动画的执行路径

flowchart TB
  classDef a1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef a2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef a3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[动画触发] --> B{执行方式}
  B -->|CSS 过渡/动画| C[浏览器合成器执行]
  B -->|JS 逐帧| D[JavaScript 计算]
  B -->|WAAPI| E[浏览器原生动画]
  C --> F{是否仅合成属性}
  F -->|transform/opacity| G[合成线程 不触发布局]
  F -->|width/height/top| H[主线程 布局重排]
  D --> I[每帧 JS 更新]
  E --> J[原生引擎 主线程外优化]

  click G "https://web.dev/animations-guide/" "动画指南"
  class A a1
  class B a1
  class C a2
  class D a2
  class E a2
  class F a2
  class G a3
  class H a3
  class I a3
  class J a3

性能铁律:动画 transform 与 opacity 由合成器处理,不触发布局与绘制;width/height/margin 等属性触发重排,应避免动画化。

二、CSS 过渡与动画:零成本方案

React 组件中的 CSS 过渡是最简单直接的方案:状态变化时切换 class,CSS 负责平滑过渡。

CSS 过渡的状态驱动

flowchart LR
  classDef c1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef c2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef c3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[状态 A<br/>isOpen=false] --> B{状态切换}
  B -->|setState 打开| C[状态 B<br/>isOpen=true]
  C --> D[class 变化]
  D --> E[CSS transition 过渡]
  B -->|setState 关闭| F[状态回 A]
  F --> G[class 还原]
  G --> E

  click E "https://react.dev/learn/you-might-not-need-an-effect" "状态驱动动画"
  class A c1
  class B c1
  class C c1
  class D c2
  class E c3
  class F c1
  class G c2

React 中 CSS 过渡示例

局限:CSS 过渡只解决「存在于 DOM 的元素的状态过渡」——元素的挂载与卸载(入场/离场)需要额外机制处理。

三、入场与离场动画:挂载卸载的难题

React 的渲染模型让「离场动画」成为天然难题:组件卸载瞬间 DOM 就没了,动画无从谈起。解决思路是「延迟卸载」——先播放离场动画,再真正卸载。

入场离场动画机制

flowchart TB
  classDef e1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef e2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef e3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[元素出现] --> B[挂载 DOM]
  B --> C[应用入场类<br/>opacity: 0 → 1]
  C --> D[transitionend 完成]
  A2[元素消失] --> E[播放离场动画]
  E --> F[动画结束回调]
  F --> G[真正卸载组件]
  G --> H[DOM 移除]

  click E "https://www.framer.com/motion/animate-presence/" "AnimatePresence 文档"
  class A e1
  class B e1
  class C e2
  class D e3
  class A2 e1
  class E e2
  class F e3
  class G e2
  class H e3

方案对比

方案 入场 离场 特点
纯 CSS + 定时器 手动 手动延迟卸载 零依赖
React Transition Group 支持 支持 经典 文档全
Framer Motion 支持 支持 声明式 现代
自研 useTransition 支持 支持 完全可控

四、Framer Motion:现代动画的标准答案

Framer Motion(现为 Motion)是目前 React 动画的事实标准——声明式动画、手势、布局动画(Layout Animation)与页面过渡开箱即用。

Framer Motion 核心 API

flowchart LR
  classDef f1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef f2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef f3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[motion.div<br/>动画组件] --> B[animate 属性<br/>目标值]
  A --> C[initial 属性<br/>初始值]
  A --> D[whileHover<br/>悬停态]
  A --> E[transition 属性<br/>时间曲线]

  F[AnimatePresence<br/>入场离场] --> G[模式切换]
  H[useScroll/useTransform<br/>滚动驱动] --> I[视差滚动]
  J[LayoutGroup<br/>布局动画] --> K[FLIP 平滑移动]

  click B "https://www.framer.com/motion/animation/" "动画文档"
  class A f1
  class B f2
  class C f2
  class D f2
  class E f2
  class F f1
  class G f3
  class H f1
  class I f3
  class J f1
  class K f3

声明式动画示例

核心价值animate/initial/exit 三态声明,React 状态驱动,入场离场天然支持——不再手动管理定时器与延迟卸载。

五、状态驱动的动画模式

React 中动画与状态紧密耦合:状态变化是动画触发源,动画完成是状态切换的时机。理解两者的时序关系至关重要。

状态与动画的时序

sequenceDiagram
  participant U as 用户
  participant S as 状态
  participant A as 动画
  participant D as DOM

  U->>S: 触发交互
  S->>A: 动画开始(旧状态视觉)
  A->>D: 过渡到新状态视觉
  A->>S: 动画完成回调
  Note over S: 状态更新与动画回调<br/>需避免竞态

动画与 Effect 的边界

flowchart TD
  classDef b1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef b2 fill:#ffebee,stroke:#c62828,color:#b71c1c
  classDef b3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[动画逻辑放置] --> B{是否与渲染无关}
  B -->|纯视觉| C[useEffect 触发]
  B -->|读取 DOM 尺寸| D[useLayoutEffect]
  B -->|渲染期修改样式| E[错误做法]
  C --> F[动画在绘制后执行]
  D --> G[动画在绘制前执行]
  E --> H[破坏声明式 禁止]

  click C "https://react.dev/reference/react/useEffect" "useEffect 文档"
  class A b1
  class B b1
  class C b3
  class D b3
  class E b2
  class F b3
  class G b3
  class H b2

六、动画性能优化

动画的流畅度取决于「每秒 60 帧」。任何超过 16.6ms 的帧都会掉帧,动画是性能问题最显性的暴露场景。

性能优化清单

flowchart TB
  classDef p1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef p2 fill:#fff3e0,stroke:#f57c00,color:#e65100
  classDef p3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[动画性能] --> B[属性选择]
  B --> B1[只用 transform/opacity]
  B --> B2[避免 width/height 动画]
  A --> C[渲染治理]
  C --> C1[动画期间避免重渲染]
  C --> C2[will-change 谨慎使用]
  A --> D[节流与降级]
  D --> D1[prefers-reduced-motion 降级]
  D --> D2[移动端简化动画]

  click B1 "https://web.dev/animations-guide/" "合成属性说明"
  class A p1
  class B p1
  class B1 p3
  class B2 p3
  class C p2
  class C1 p3
  class C2 p3
  class D p2
  class D1 p3
  class D2 p3

合成器友好的动画

flowchart LR
  classDef y1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef y2 fill:#ffebee,stroke:#c62828,color:#b71c1c
  classDef y3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[动画属性] --> B[合成器友好<br/>transform opacity]
  A --> C[触发重排<br/>width height top]
  A --> D[触发重绘<br/>color box-shadow]

  click B "https://developer.mozilla.org/zh-CN/docs/Web/Performance/CSS_JavaScript_animation_performance" "动画性能文档"
  class A y1
  class B y3
  class C y2
  class D y2

七、无障碍与动画降级

动画不是「必须」,而是「增强」。尊重用户的系统偏好与生理需求,是动画方案不可忽视的一环。

动画降级策略

flowchart TD
  classDef g1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef g2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef g3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[用户偏好检测] --> B{prefers-reduced-motion}
  B -->|减少动态| C[禁用非必要动画]
  B -->|无偏好| D[完整动画]
  C --> E[保留必要过渡<br/>透明度淡入]
  C --> F[移除位移/缩放]
  D --> G[完整动效]

  click B "https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-reduced-motion" "reduce-motion 文档"
  class A g1
  class B g1
  class C g2
  class D g3
  class E g3
  class F g2
  class G g3

实践:Framer Motion 提供了 MotionConfig reducedMotion="user" 一键尊重系统偏好;纯 CSS 方案用媒体查询包裹动画声明。

八、总结

React 动画的实现有一条清晰的决策路径:

  1. 简单状态过渡:CSS 过渡即可,零成本;
  2. 入场离场:需要延迟卸载机制,Framer Motion 的 AnimatePresence 最省心;
  3. 手势/布局动画:Framer Motion 声明式覆盖;
  4. 性能铁律:只用 transform/opacity,尊重合成器;
  5. 无障碍:prefers-reduced-motion 降级是底线。

实践建议:从 CSS 过渡起步,遇到「离场动画、布局动画、手势」需求时引入 Framer Motion;动画永远服务于「让状态变化更可理解」的目标,克制才是高级。下一篇我们将探讨 React 微前端架构的完整方案。