
React 应用为何变慢
React 默认很快,但某些模式会导致不必要的工作:
- 不必要的重渲染 — 组件在输出不会改变时重新渲染
- 每次渲染都进行昂贵计算 — 不必要地重新计算派生数据
- 包体积过大 — 加载用户尚未需要的代码
- 长列表渲染 — 一次性渲染数千个 DOM 节点
- 布局抖动 — JS 读写操作迫使浏览器反复重新计算布局
本指南是一份实用清单,而非理论。
第一步:先测量再优化
切勿盲目优化。首先使用这些工具:
// React DevTools Profiler
// 1. 安装 React DevTools 浏览器扩展
// 2. 打开 DevTools → Profiler 标签
// 3. 记录用户交互
// 4. 查找频繁渲染或耗时长的组件
// 在开发环境中检测不必要的重渲染
// 将此代码添加到组件以查看其渲染时机:
const MyComponent = ({ value }) => {
console.log('MyComponent rendered:', value);
return <div>{value}</div>;
};
// 或使用 react-why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, { trackAllPureComponents: true });
第二步:修复不必要的重渲染
React.memo — 记忆化组件
// 不使用 memo:父组件重渲染时子组件总是重渲染
const UserCard = ({ user }) => (
<div>{user.name} - {user.email}</div>
);
// 使用 memo:仅当 user prop 变化时重渲染(浅比较)
const UserCard = React.memo(({ user }) => (
<div>{user.name} - {user.email}</div>
));
// 自定义比较(当浅比较不够用时)
const UserCard = React.memo(
({ user }) => <div>{user.name}</div>,
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
何时使用: 频繁渲染但接收相同 props 的组件——列表项、图表节点、表格行。
何时不使用: 很少渲染的简单组件。记忆化开销可能比重渲染成本更高。
稳定的回调引用
// ❌ 每次渲染创建新函数 → 记忆化子组件总是重渲染
function ParentList() {
const [items, setItems] = useState([]);
return items.map(item => (
<MemoizedItem
key={item.id}
item={item}
onDelete={() => setItems(prev => prev.filter(i => i.id !== item.id))} // 每次渲染新函数
/>
));
}
// ✅ 使用 useCallback 保持稳定回调
function ParentList() {
const [items, setItems] = useState([]);
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []); // 稳定引用
return items.map(item => (
<MemoizedItem key={item.id} item={item} onDelete={handleDelete} />
));
}
稳定的对象引用
// ❌ 每次渲染创建新对象
function Chart({ data }) {
const options = { color: 'blue', animated: true }; // 每次渲染新引用
return <MemoizedChart data={data} options={options} />; // 总是重渲染
}
// ✅ 记忆化 options 对象
function Chart({ data }) {
const options = useMemo(
() => ({ color: 'blue', animated: true }),
[] // 永不改变——也可以作为模块级常量
);
return <MemoizedChart data={data} options={options} />;
}
第三步:记忆化昂贵计算
// ❌ 每次渲染过滤 10,000 个项目
function ProductList({ products, search, category, maxPrice }) {
const filtered = products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.filter(p => category ? p.category === category : true)
.filter(p => p.price <= maxPrice)
.sort((a, b) => a.price - b.price);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
// ✅ 仅在输入变化时重新计算
function ProductList({ products, search, category, maxPrice }) {
const filtered = useMemo(
() => products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.filter(p => category ? p.category === category : true)
.filter(p => p.price <= maxPrice)
.sort((a, b) => a.price - b.price),
[products, search, category, maxPrice]
);
return filtered.map(p => <ProductCard key={p.id} product={p} />);
}
第四步:状态架构
就近放置状态
将状态尽可能靠近其使用位置。全局状态会导致广泛的重渲染。
// ❌ 工具提示打开状态放在全局 store → 每次悬停所有组件重渲染
const { isTooltipOpen, setTooltipOpen } = useGlobalStore();
// ✅ 工具提示状态局部于工具提示组件
function TooltipWrapper({ children, content }) {
const [isOpen, setIsOpen] = useState(false); // 仅此组件重渲染
return (
<div onMouseEnter={() => setIsOpen(true)} onMouseLeave={() => setIsOpen(false)}>
{children}
{isOpen && <Tooltip>{content}</Tooltip>}
</div>
);
}
拆分 Context
// ❌ 一个 context 包含所有内容——任何更新都会重渲染所有消费者
const AppContext = createContext({ user, theme, cart, notifications });
// ✅ 拆分为独立的 context
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
const CartContext = createContext(cart);
// 组件只订阅所需内容
function PriceTag() {
const cart = useContext(CartContext); // 仅在 cart 变化时重渲染
return <span>{cart.total}</span>;
}
第五步:列表虚拟化
渲染 1000+ 行会创建 1000+ 个 DOM 节点。虚拟化只渲染可见部分。
import { FixedSizeList as List } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-item">
{items[index].name}
</div>
);
return (
<List
height={600} // 可见容器高度
itemCount={items.length}
itemSize={50} // 每个项目高度
width="100%"
>
{Row}
</List>
);
}
// 对于可变高度项目
import { VariableSizeList } from 'react-window';
对于简单列表使用 react-window,对于更复杂的情况(包括网格和瀑布流布局)使用 react-virtual(TanStack Virtual)。
第六步:代码分割
// ❌ 所有内容在一个包中
import HeavyEditor from './HeavyEditor';
import Chart from './Chart';
// ✅ 动态导入——仅在需要时加载
import { lazy, Suspense } from 'react';
const HeavyEditor = lazy(() => import('./HeavyEditor'));
const Chart = lazy(() => import('./Chart'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/editor" element={<HeavyEditor />} />
<Route path="/analytics" element={<Chart />} />
</Routes>
</Suspense>
);
}
悬停时预取
// 在用户点击前预加载代码块
function NavLink({ to, children }) {
const prefetch = () => import('./HeavyPage'); // 触发代码块下载
return (
<Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
{children}
</Link>
);
}
第七步:图片与资源优化
// 懒加载折叠线以下的图片
function ProductImage({ src, alt }) {
return (
<img
src={src}
alt={alt}
loading="lazy" // 原生懒加载
decoding="async" // 解码时不阻塞渲染
width={300}
height={200} // 防止布局偏移
/>
);
}
// 对于 Next.js — 使用 next/image
import Image from 'next/image';
<Image src={src} alt={alt} width={300} height={200} priority={false} />
第八步:React 18 并发特性
import { useTransition, useDeferredValue } from 'react';
// useTransition:将昂贵的状态更新标记为非紧急
function SearchBox({ items }) {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleSearch(e) {
setQuery(e.target.value); // 紧急:立即更新输入
startTransition(() => {
// 非紧急:如果用户再次输入可中断
setFilteredItems(items.filter(i => i.includes(e.target.value)));
});
}
return (
<>
<input value={query} onChange={handleSearch} />
{isPending && <Spinner />}
<List items={filteredItems} />
</>
);
}
// useDeferredValue:延迟派生值
function SearchResults({ query, items }) {
const deferredQuery = useDeferredValue(query); // 快速输入时滞后
const filtered = useMemo(
() => items.filter(i => i.includes(deferredQuery)),
[items, deferredQuery]
);
return <List items={filtered} />;
}
性能清单
- 先分析——确定实际瓶颈
- 对频繁接收稳定 props 的组件使用 memo
- 对传递给记忆化子组件的回调使用
useCallback - 对昂贵的过滤/排序列表使用
useMemo - 就近放置状态——避免将临时 UI 状态放在全局
- 拆分 context——分离关注点
- 虚拟化长列表(1000+ 项)
- 对路由和重型组件进行代码分割
- 使用
loading="lazy"懒加载图片 - 对慢状态更新使用
useTransition(React 18+)
→ 使用 Benchmark Builder 进行基准测试并测量性能指标。