React 动画实现方案
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 过渡示例
javascript1234567891011function Accordion({ children }) { const [open, setOpen] = useState(false); return ( <div className={`accordion ${open ? "is-open" : ""}`}> <button onClick={() => setOpen(!open)}>展开</button> <div className="accordion__panel"> {children} </div> </div> ); }
css12345678.accordion__panel { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } .accordion.is-open .accordion__panel { max-height: 500px; }
局限: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
声明式动画示例
javascript123456789101112131415161718192021222324252627import { motion, AnimatePresence } from "framer-motion"; function ToggleCard() { const [show, setShow] = useState(true); return ( <> <motion.button whileTap={{ scale: 0.95 }} onClick={() => setShow(!show)} > 切换 </motion.button> <AnimatePresence> {show && ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3 }} > 卡片内容 </motion.div> )} </AnimatePresence> </> ); }
核心价值: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 动画的实现有一条清晰的决策路径:
- 简单状态过渡:CSS 过渡即可,零成本;
- 入场离场:需要延迟卸载机制,Framer Motion 的 AnimatePresence 最省心;
- 手势/布局动画:Framer Motion 声明式覆盖;
- 性能铁律:只用 transform/opacity,尊重合成器;
- 无障碍:prefers-reduced-motion 降级是底线。
实践建议:从 CSS 过渡起步,遇到「离场动画、布局动画、手势」需求时引入 Framer Motion;动画永远服务于「让状态变化更可理解」的目标,克制才是高级。下一篇我们将探讨 React 微前端架构的完整方案。
