Appearance
React与CSS动画结合
概述
在React应用中优雅地集成CSS动画需要深入理解React的组件生命周期、状态管理以及DOM操作时机。本文将全面讲解如何在React中高效使用CSS动画,包括进入/退出动画、条件渲染动画、路由切换动画等实战场景,帮助你构建流畅的用户界面。
React动画基础
组件挂载/卸载动画
基础实现
tsx
import { useState, useEffect } from 'react';
import './Modal.css';
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const [shouldRender, setShouldRender] = useState(false);
useEffect(() => {
if (isOpen) {
setShouldRender(true);
}
}, [isOpen]);
const handleAnimationEnd = () => {
if (!isOpen) {
setShouldRender(false);
}
};
if (!shouldRender) return null;
return (
<div
className={`modal-overlay ${isOpen ? 'open' : 'close'}`}
onAnimationEnd={handleAnimationEnd}
onClick={onClose}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
<button onClick={onClose}>Close</button>
</div>
</div>
);
}css
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(20px);
opacity: 0;
}
}
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.open {
animation: fadeIn 0.3s ease-out;
}
.modal-overlay.close {
animation: fadeOut 0.3s ease-out;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
.modal-overlay.open .modal-content {
animation: slideUp 0.3s ease-out;
}
.modal-overlay.close .modal-content {
animation: slideDown 0.3s ease-out;
}自定义Hook封装
tsx
function useAnimatedUnmount(visible: boolean, unmountDelay = 300) {
const [shouldRender, setShouldRender] = useState(visible);
useEffect(() => {
if (visible) {
setShouldRender(true);
} else {
const timer = setTimeout(() => {
setShouldRender(false);
}, unmountDelay);
return () => clearTimeout(timer);
}
}, [visible, unmountDelay]);
return shouldRender;
}
// 使用
function AnimatedComponent({ isVisible }: { isVisible: boolean }) {
const shouldRender = useAnimatedUnmount(isVisible, 300);
if (!shouldRender) return null;
return (
<div className={`animated ${isVisible ? 'enter' : 'exit'}`}>
Content
</div>
);
}列表动画
交错进入动画
tsx
interface Item {
id: number;
text: string;
}
function StaggeredList({ items }: { items: Item[] }) {
const [visibleItems, setVisibleItems] = useState<number[]>([]);
useEffect(() => {
items.forEach((item, index) => {
setTimeout(() => {
setVisibleItems(prev => [...prev, item.id]);
}, index * 100);
});
}, [items]);
return (
<ul className="staggered-list">
{items.map((item) => (
<li
key={item.id}
className={`list-item ${visibleItems.includes(item.id) ? 'visible' : ''}`}
>
{item.text}
</li>
))}
</ul>
);
}css
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.list-item {
opacity: 0;
transform: translateX(-20px);
}
.list-item.visible {
animation: slideInLeft 0.3s ease-out forwards;
}列表项删除动画
tsx
function AnimatedList() {
const [items, setItems] = useState([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' },
]);
const [removingId, setRemovingId] = useState<number | null>(null);
const removeItem = (id: number) => {
setRemovingId(id);
setTimeout(() => {
setItems(items => items.filter(item => item.id !== id));
setRemovingId(null);
}, 300);
};
return (
<ul>
{items.map((item) => (
<li
key={item.id}
className={`item ${removingId === item.id ? 'removing' : ''}`}
>
<span>{item.text}</span>
<button onClick={() => removeItem(item.id)}>×</button>
</li>
))}
</ul>
);
}css
@keyframes slideOutRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
.item {
transition: margin 0.3s ease;
}
.item.removing {
animation: slideOutRight 0.3s ease-out forwards;
margin-bottom: 0 !important;
}React Transition Group
CSSTransition组件
tsx
import { useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './Transitions.css';
function AlertBox() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>
Toggle Alert
</button>
<CSSTransition
in={show}
timeout={300}
classNames="alert"
unmountOnExit
>
<div className="alert">
<p>This is an alert message!</p>
<button onClick={() => setShow(false)}>Dismiss</button>
</div>
</CSSTransition>
</div>
);
}css
/* 进入开始 */
.alert-enter {
opacity: 0;
transform: scale(0.9);
}
/* 进入活跃状态 */
.alert-enter-active {
opacity: 1;
transform: scale(1);
transition: opacity 300ms, transform 300ms;
}
/* 退出开始 */
.alert-exit {
opacity: 1;
transform: scale(1);
}
/* 退出活跃状态 */
.alert-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}TransitionGroup列表动画
tsx
import { TransitionGroup, CSSTransition } from 'react-transition-group';
interface Todo {
id: number;
text: string;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, { id: Date.now(), text: inputValue }]);
setInputValue('');
}
};
const removeTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<div className="input-group">
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
</div>
<TransitionGroup className="todo-list">
{todos.map((todo) => (
<CSSTransition
key={todo.id}
timeout={500}
classNames="todo"
>
<div className="todo-item">
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>×</button>
</div>
</CSSTransition>
))}
</TransitionGroup>
</div>
);
}css
.todo-list {
list-style: none;
padding: 0;
}
.todo-enter {
opacity: 0;
transform: translateY(-10px);
}
.todo-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 500ms, transform 500ms;
}
.todo-exit {
opacity: 1;
transform: translateX(0);
}
.todo-exit-active {
opacity: 0;
transform: translateX(100%);
transition: opacity 500ms, transform 500ms;
}
.todo-item {
padding: 12px;
margin-bottom: 8px;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}路由切换动画
React Router动画
tsx
import { Routes, Route, useLocation } from 'react-router-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
function AnimatedRoutes() {
const location = useLocation();
return (
<TransitionGroup>
<CSSTransition
key={location.key}
classNames="page"
timeout={300}
>
<Routes location={location}>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</CSSTransition>
</TransitionGroup>
);
}css
.page-enter {
opacity: 0;
transform: translateX(100%);
}
.page-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.page-exit {
opacity: 1;
transform: translateX(0);
}
.page-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: opacity 300ms, transform 300ms;
}方向感知切换
tsx
function DirectionalRouteTransition() {
const location = useLocation();
const [direction, setDirection] = useState<'forward' | 'backward'>('forward');
const prevLocationRef = useRef(location);
useEffect(() => {
const routeOrder = ['/', '/page1', '/page2', '/page3'];
const prevIndex = routeOrder.indexOf(prevLocationRef.current.pathname);
const currentIndex = routeOrder.indexOf(location.pathname);
setDirection(currentIndex > prevIndex ? 'forward' : 'backward');
prevLocationRef.current = location;
}, [location]);
return (
<TransitionGroup>
<CSSTransition
key={location.key}
classNames={`page-${direction}`}
timeout={300}
>
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/page1" element={<Page1 />} />
<Route path="/page2" element={<Page2 />} />
<Route path="/page3" element={<Page3 />} />
</Routes>
</CSSTransition>
</TransitionGroup>
);
}css
/* 前进动画 */
.page-forward-enter {
transform: translateX(100%);
}
.page-forward-enter-active {
transform: translateX(0);
transition: transform 300ms ease-out;
}
.page-forward-exit {
transform: translateX(0);
}
.page-forward-exit-active {
transform: translateX(-100%);
transition: transform 300ms ease-out;
}
/* 后退动画 */
.page-backward-enter {
transform: translateX(-100%);
}
.page-backward-enter-active {
transform: translateX(0);
transition: transform 300ms ease-out;
}
.page-backward-exit {
transform: translateX(0);
}
.page-backward-exit-active {
transform: translateX(100%);
transition: transform 300ms ease-out;
}高级动画模式
共享元素过渡
tsx
interface CardData {
id: number;
title: string;
image: string;
}
function SharedElementTransition() {
const [selectedCard, setSelectedCard] = useState<CardData | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
const handleCardClick = (card: CardData) => {
setIsAnimating(true);
setSelectedCard(card);
setTimeout(() => {
setIsAnimating(false);
}, 300);
};
const handleClose = () => {
setIsAnimating(true);
setTimeout(() => {
setSelectedCard(null);
setIsAnimating(false);
}, 300);
};
return (
<div className="shared-transition-container">
{!selectedCard && (
<div className="card-grid">
{cards.map((card) => (
<div
key={card.id}
className="card"
onClick={() => handleCardClick(card)}
>
<img src={card.image} alt={card.title} />
<h3>{card.title}</h3>
</div>
))}
</div>
)}
{selectedCard && (
<div
className={`detail-view ${isAnimating ? 'animating' : ''}`}
onClick={handleClose}
>
<div className="detail-content">
<img src={selectedCard.image} alt={selectedCard.title} />
<h2>{selectedCard.title}</h2>
<p>Detailed content here...</p>
</div>
</div>
)}
</div>
);
}css
@keyframes expandCard {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.detail-view.animating {
animation: expandCard 0.3s ease-out;
}
.detail-content img {
width: 100%;
max-width: 600px;
border-radius: 8px;
}序列化动画
tsx
function SequentialAnimation() {
const [activeStep, setActiveStep] = useState(0);
const steps = ['Step 1', 'Step 2', 'Step 3', 'Step 4'];
useEffect(() => {
const timer = setInterval(() => {
setActiveStep((step) => (step + 1) % steps.length);
}, 2000);
return () => clearInterval(timer);
}, []);
return (
<div className="sequence-container">
{steps.map((step, index) => (
<div
key={index}
className={`step ${index === activeStep ? 'active' : ''} ${
index < activeStep ? 'completed' : ''
}`}
style={{ animationDelay: `${index * 0.1}s` }}
>
{step}
</div>
))}
</div>
);
}css
@keyframes stepActivate {
0% {
transform: scale(1);
background-color: #e5e7eb;
}
50% {
transform: scale(1.1);
background-color: #3b82f6;
}
100% {
transform: scale(1);
background-color: #3b82f6;
}
}
.step {
padding: 16px;
border-radius: 8px;
background-color: #e5e7eb;
margin-bottom: 12px;
transition: background-color 0.3s;
}
.step.active {
animation: stepActivate 0.5s ease;
color: white;
}
.step.completed {
background-color: #10b981;
color: white;
}视差滚动动画
tsx
function ParallaxScroll() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div className="parallax-container">
<div
className="parallax-bg"
style={{
transform: `translateY(${scrollY * 0.5}px)`,
}}
/>
<div
className="parallax-content"
style={{
transform: `translateY(${scrollY * 0.3}px)`,
}}
>
<h1>Parallax Title</h1>
<p>Scroll to see the effect</p>
</div>
</div>
);
}css
.parallax-container {
height: 200vh;
position: relative;
overflow: hidden;
}
.parallax-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
z-index: -1;
}
.parallax-content {
position: relative;
padding: 100px 20px;
color: white;
text-align: center;
}实战案例
1. 抽屉菜单
tsx
function Drawer({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const drawerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
return (
<>
{isOpen && (
<div className="drawer-overlay" onClick={onClose}>
<div
ref={drawerRef}
className={`drawer ${isOpen ? 'open' : ''}`}
onClick={(e) => e.stopPropagation()}
>
<div className="drawer-header">
<h2>Menu</h2>
<button onClick={onClose}>×</button>
</div>
<div className="drawer-content">
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</div>
</div>
</div>
)}
</>
);
}css
@keyframes slideInLeft {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.drawer-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
animation: fadeIn 0.3s ease;
}
.drawer {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 300px;
background-color: white;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
animation: slideInLeft 0.3s ease-out;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e5e7eb;
}
.drawer-content nav {
display: flex;
flex-direction: column;
padding: 20px;
}
.drawer-content nav a {
padding: 12px;
text-decoration: none;
color: #374151;
transition: background-color 0.2s;
}
.drawer-content nav a:hover {
background-color: #f3f4f6;
}2. 折叠面板
tsx
interface AccordionItem {
id: number;
title: string;
content: string;
}
function Accordion({ items }: { items: AccordionItem[] }) {
const [openId, setOpenId] = useState<number | null>(null);
const toggleItem = (id: number) => {
setOpenId(openId === id ? null : id);
};
return (
<div className="accordion">
{items.map((item) => (
<div key={item.id} className="accordion-item">
<button
className="accordion-header"
onClick={() => toggleItem(item.id)}
>
<span>{item.title}</span>
<span className={`arrow ${openId === item.id ? 'open' : ''}`}>
▼
</span>
</button>
<div
className={`accordion-content ${openId === item.id ? 'open' : ''}`}
>
<div className="accordion-body">{item.content}</div>
</div>
</div>
))}
</div>
);
}css
.accordion-item {
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
}
.accordion-header {
width: 100%;
padding: 16px;
background-color: #f9fafb;
border: none;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: background-color 0.2s;
}
.accordion-header:hover {
background-color: #f3f4f6;
}
.arrow {
transition: transform 0.3s ease;
}
.arrow.open {
transform: rotate(180deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion-content.open {
max-height: 500px;
}
.accordion-body {
padding: 16px;
}3. 图片轮播
tsx
function Carousel({ images }: { images: string[] }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [direction, setDirection] = useState<'next' | 'prev'>('next');
const goToNext = () => {
setDirection('next');
setCurrentIndex((index) => (index + 1) % images.length);
};
const goToPrev = () => {
setDirection('prev');
setCurrentIndex((index) => (index - 1 + images.length) % images.length);
};
useEffect(() => {
const timer = setInterval(goToNext, 5000);
return () => clearInterval(timer);
}, []);
return (
<div className="carousel">
<button className="carousel-button prev" onClick={goToPrev}>
‹
</button>
<div className="carousel-track">
{images.map((image, index) => (
<img
key={index}
src={image}
alt={`Slide ${index + 1}`}
className={`carousel-image ${
index === currentIndex
? `active ${direction}`
: index === (currentIndex - 1 + images.length) % images.length
? 'prev'
: 'next'
}`}
/>
))}
</div>
<button className="carousel-button next" onClick={goToNext}>
›
</button>
<div className="carousel-dots">
{images.map((_, index) => (
<button
key={index}
className={`dot ${index === currentIndex ? 'active' : ''}`}
onClick={() => {
setDirection(index > currentIndex ? 'next' : 'prev');
setCurrentIndex(index);
}}
/>
))}
</div>
</div>
);
}css
@keyframes slideInFromRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInFromLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.carousel {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
overflow: hidden;
border-radius: 12px;
}
.carousel-track {
position: relative;
height: 400px;
}
.carousel-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s;
}
.carousel-image.active.next {
animation: slideInFromRight 0.5s ease forwards;
opacity: 1;
}
.carousel-image.active.prev {
animation: slideInFromLeft 0.5s ease forwards;
opacity: 1;
}
.carousel-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.8);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 24px;
cursor: pointer;
z-index: 10;
transition: background-color 0.2s;
}
.carousel-button:hover {
background-color: white;
}
.carousel-button.prev { left: 20px; }
.carousel-button.next { right: 20px; }
.carousel-dots {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
border: none;
cursor: pointer;
transition: background-color 0.3s, transform 0.3s;
}
.dot.active {
background-color: white;
transform: scale(1.2);
}4. 下拉菜单
tsx
function Dropdown({ trigger, items }: {
trigger: React.ReactNode;
items: Array<{ label: string; onClick: () => void }>;
}) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="dropdown" ref={dropdownRef}>
<button className="dropdown-trigger" onClick={() => setIsOpen(!isOpen)}>
{trigger}
</button>
{isOpen && (
<div className="dropdown-menu">
{items.map((item, index) => (
<button
key={index}
className="dropdown-item"
onClick={() => {
item.onClick();
setIsOpen(false);
}}
style={{ animationDelay: `${index * 0.05}s` }}
>
{item.label}
</button>
))}
</div>
)}
</div>
);
}css
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes dropdownItemSlide {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-trigger {
padding: 8px 16px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.dropdown-trigger:hover {
background-color: #2563eb;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 8px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
min-width: 200px;
overflow: hidden;
z-index: 1000;
animation: dropdownFadeIn 0.2s ease-out;
}
.dropdown-item {
display: block;
width: 100%;
padding: 12px 16px;
text-align: left;
background: none;
border: none;
cursor: pointer;
transition: background-color 0.2s;
animation: dropdownItemSlide 0.3s ease-out backwards;
}
.dropdown-item:hover {
background-color: #f3f4f6;
}性能优化
动画性能监控
tsx
function AnimationPerformanceMonitor({ children }: { children: React.ReactNode }) {
useEffect(() => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'measure') {
console.log(`Animation ${entry.name}: ${entry.duration}ms`);
}
}
});
observer.observe({ entryTypes: ['measure'] });
return () => observer.disconnect();
}, []);
return <>{children}</>;
}使用requestAnimationFrame
tsx
function useRAF(callback: () => void, deps: any[]) {
const requestRef = useRef<number>();
useEffect(() => {
const animate = () => {
callback();
requestRef.current = requestAnimationFrame(animate);
};
requestRef.current = requestAnimationFrame(animate);
return () => {
if (requestRef.current) {
cancelAnimationFrame(requestRef.current);
}
};
}, deps);
}
// 使用
function SmoothCounter() {
const [target, setTarget] = useState(100);
const [current, setCurrent] = useState(0);
useRAF(() => {
setCurrent((prev) => {
const diff = target - prev;
if (Math.abs(diff) < 0.1) return target;
return prev + diff * 0.1;
});
}, [target]);
return (
<div>
<div>Count: {Math.round(current)}</div>
<button onClick={() => setTarget(Math.random() * 1000)}>
Random Target
</button>
</div>
);
}最佳实践总结
开发清单
✅ 使用CSS动画代替JavaScript动画
✅ 优先使用transform和opacity
✅ 合理使用React Transition Group
✅ 为列表项添加唯一key
✅ 使用will-change提示优化
✅ 避免在渲染时创建动画
✅ 清理定时器和事件监听器
✅ 测试动画在不同设备的表现性能优化
✅ 使用requestAnimationFrame同步动画
✅ 避免强制同步布局
✅ 使用CSS containment隔离
✅ 限制同时动画的元素数量
✅ 为长列表使用虚拟滚动
✅ 延迟加载非关键动画
✅ 监控动画性能指标可访问性
✅ 支持prefers-reduced-motion
✅ 提供跳过动画选项
✅ 确保键盘导航正常
✅ 为屏幕阅读器提供适当提示
✅ 测试高对比度模式React与CSS动画的结合为构建流畅、响应式的用户界面提供了强大工具。通过掌握组件生命周期、合理使用动画库、优化性能,可以创建出色的动画体验。