引言
React 的性能优化是一个被广泛讨论但经常被误解的话题。许多开发者过早地使用 memo、useMemo 和 useCallback,不仅没有带来性能提升,反而增加了代码复杂度和维护成本。
本文将深入 React 的渲染机制,系统地梳理性能优化的优先级和实战技巧——从理解什么是"不必要的渲染",到虚拟列表、代码分割等重量级优化手段。
一、理解 React 的渲染机制
在讨论优化之前,必须理解 React 何时会触发重新渲染。
渲染的触发条件
React 组件在以下三种情况下会重新渲染:
- State 发生变化 — 组件自身的 state 通过
setState或useState更新 - 父组件重新渲染 — 即使传给子组件的 props 没有变化,子组件默认也会重新渲染
- Context 值变化 — 组件消费的 Context 值发生改变
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
{/* 即使 ExpensiveChild 不需要 count,它也会重新渲染 */}
<ExpensiveChild />
</div>
);
}渲染 !== DOM 更新
一个重要认知:React 的"渲染"是生成 Virtual DOM 的过程,不等于实际的 DOM 操作。React 通过 Reconciliation 算法(diffing)来决定哪些 DOM 需要更新。渲染的开销主要在于组件函数的执行和 Virtual DOM 的比较,而非 DOM 操作本身。
二、React.memo:阻止不必要的渲染
React.memo 是最基础也是最常用的性能优化 API。它通过浅比较 props 来决定是否跳过渲染。
基础用法
import { memo } from 'react';
interface UserCardProps {
name: string;
avatar: string;
bio: string;
}
const UserCard = memo(function UserCard({ name, avatar, bio }: UserCardProps) {
console.log('UserCard rendered');
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{bio}</p>
</div>
);
});自定义比较函数
当浅比较不够用时,可以传入自定义比较函数:
const UserCard = memo(
function UserCard({ user, onSelect }: UserCardProps) {
return (
<div onClick={() => onSelect(user.id)}>
<h3>{user.name}</h3>
</div>
);
},
(prevProps, nextProps) => {
// 仅当 user.id 或 onSelect 引用变化时才重新渲染
return (
prevProps.user.id === nextProps.user.id &&
prevProps.onSelect === nextProps.onSelect
);
}
);什么时候不该用 memo?
并非所有组件都需要 memo:
| 场景 | 是否使用 memo | 原因 |
|---|---|---|
| 组件 props 频繁变化 | ❌ 不需要 | memo 的比较开销反而增加成本 |
| 组件本身渲染开销很小 | ❌ 不需要 | 浅比较成本可能超过重渲染成本 |
| props 中有 children | ⚠️ 通常不需要 | children 每次都是新引用,memo 几乎无效 |
| 渲染开销大的纯展示组件 | ✅ 推荐 | 避免不必要的重复计算 |
| 大型列表中项组件 | ✅ 强烈推荐 | 配合虚拟列表效果显著 |
三、useMemo:缓存计算值
useMemo 用来缓存昂贵的计算结果,避免每次渲染都重新计算。
正确的使用场景
function ProductList({ products, filters }: Props) {
// ✅ 过滤和排序是昂贵操作,用 useMemo 缓存
const filteredProducts = useMemo(() => {
return products
.filter((p) => {
if (filters.category && p.category !== filters.category) return false;
if (filters.minPrice && p.price < filters.minPrice) return false;
if (filters.maxPrice && p.price > filters.maxPrice) return false;
return true;
})
.sort((a, b) => {
switch (filters.sortBy) {
case 'price-asc': return a.price - b.price;
case 'price-desc': return b.price - a.price;
case 'rating': return b.rating - a.rating;
default: return 0;
}
});
}, [products, filters]);
return (
<ul>
{filteredProducts.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}useMemo 的常见误用
// ❌ 错误:缓存一个原始值或简单的对象字面量
const count = useMemo(() => 1 + 2, []);
const style = useMemo(() => ({ color: 'red' }), []);
// ✅ 正确:直接声明即可
const count = 1 + 2;
// ✅ 正确:真正需要缓存的场景——传给 memo 组件的引用类型 prop
const sortedItems = useMemo(() => {
return items.slice().sort(complexSortFn);
}, [items]);useMemo 与引用稳定性
useMemo 的一个重要用途是保持引用稳定,配合 React.memo 使用:
function Dashboard({ rawData }: Props) {
// 保持 chartData 引用稳定,避免子组件无意义重渲染
const chartData = useMemo(() => {
return processChartData(rawData);
}, [rawData]);
return <ExpensiveChart data={chartData} />;
}四、useCallback:缓存函数引用
useCallback 是 useMemo 的特化版本,专门用于缓存函数。
核心使用场景
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// ✅ 用 useCallback 保证 handleSearch 引用稳定
const handleSearch = useCallback(async (searchQuery: string) => {
const data = await fetch(`/api/search?q=${searchQuery}`).then((r) => r.json());
setResults(data);
}, []); // 依赖为空,函数引用永远不变
return (
<>
<SearchInput onSearch={handleSearch} />
<SearchResults results={results} />
</>
);
}useCallback + memo 的组合模式
const ExpensiveList = memo(function ExpensiveList({
items,
onItemClick,
}: {
items: Item[];
onItemClick: (id: string) => void;
}) {
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
function Parent() {
const [items, setItems] = useState<Item[]>([]);
// 如果不使用 useCallback,每次渲染 handleClick 都是新函数
// → ExpensiveList 的 memo 失效 → 每次都重新渲染
const handleClick = useCallback((id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);
return <ExpensiveList items={items} onItemClick={handleClick} />;
}当 state 依赖不可避免时
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// ✅ 使用函数式更新避免依赖 todos
const handleToggle = useCallback((id: string) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []); // 依赖为空
// ❌ 这会导致 handleDelete 每次 todos 变化时都创建新引用
const handleDelete = useCallback(
(id: string) => {
setTodos(todos.filter((t) => t.id !== id));
},
[todos] // 依赖变化频繁
);
}五、虚拟列表:渲染海量数据
当列表包含数百甚至数千项时,虚拟列表是解决性能问题的终极方案。
实现原理
虚拟列表的核心思想是只渲染可视区域内的元素:
┌─────────────────────┐
│ (hidden padding) │ ← 滚动偏移量
├─────────────────────┤
│ Item 47 │ ← 可视区域
│ Item 48 │
│ Item 49 │
│ Item 50 │
│ Item 51 │
├─────────────────────┤
│ (hidden padding) │ ← 剩余高度
└─────────────────────┘
使用 react-window
import { FixedSizeList as List } from 'react-window';
interface RowProps {
index: number;
style: React.CSSProperties;
data: Item[];
}
const Row = memo(function Row({ index, style, data }: RowProps) {
const item = data[index];
return (
<div style={style} className="list-item">
<span className="index">{index + 1}</span>
<span className="name">{item.name}</span>
<span className="value">{item.value}</span>
</div>
);
});
function VirtualList({ items }: { items: Item[] }) {
return (
<List
height={600}
itemCount={items.length}
itemSize={48}
width="100%"
itemData={items}
>
{Row}
</List>
);
}动态高度的虚拟列表
import { VariableSizeList as List } from 'react-window';
function DynamicVirtualList({ items }: { items: Post[] }) {
const listRef = useRef<List>(null);
const itemSizes = useRef<Map<number, number>>(new Map());
const getItemSize = useCallback((index: number) => {
return itemSizes.current.get(index) ?? 100; // 默认高度
}, []);
const Row = memo(function Row({ index, style, data }: RowProps) {
return (
<div style={style}>
<h3>{data[index].title}</h3>
<p>{data[index].body}</p>
</div>
);
});
return (
<List
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
itemData={items}
>
{Row}
</List>
);
}自定义简易虚拟列表
理解原理有助于更好地使用现成库:
function SimpleVirtualList({ items, itemHeight, containerHeight }: Props) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2);
const endIndex = Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + 2
);
const visibleItems = items.slice(startIndex, endIndex);
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}六、代码分割与懒加载
React.lazy 与 Suspense
import { lazy, Suspense } from 'react';
// 路由级代码分割
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}组件级懒加载
const HeavyChart = lazy(() => import('./HeavyChart'));
const CodeEditor = lazy(() => import('./CodeEditor'));
function ContentPanel() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<Suspense fallback={<div className="chart-skeleton" />}>
<HeavyChart data={chartData} />
</Suspense>
<button onClick={() => setShowEditor(true)}>打开编辑器</button>
{showEditor && (
<Suspense fallback={<div>加载中...</div>}>
<CodeEditor />
</Suspense>
)}
</div>
);
}条件加载重型依赖
async function loadMarkdownEditor() {
const [{ default: Editor }, { default: highlight }] = await Promise.all([
import('@uiw/react-md-editor'),
import('highlight.js'),
]);
return { Editor, highlight };
}
function MarkdownSection() {
const [isEditing, setIsEditing] = useState(false);
const [EditorComp, setEditorComp] = useState<React.ComponentType | null>(null);
const enableEditing = async () => {
setIsEditing(true);
const { Editor } = await loadMarkdownEditor();
setEditorComp(() => Editor);
};
if (!isEditing) {
return <button onClick={enableEditing}>编辑</button>;
}
if (!EditorComp) return <div className="editor-loading" />;
return <EditorComp value={content} onChange={setContent} />;
}七、状态管理优化
Context 拆分
将不同关注点的 Context 分开,减少无关组件的重渲染:
// ❌ 单一 Context 导致所有消费者在任一值变化时都重渲染
const AppContext = createContext({
user: null as User | null,
theme: 'light' as Theme,
notifications: [] as Notification[],
});
// ✅ 拆分为独立 Context
const UserContext = createContext<User | null>(null);
const ThemeContext = createContext<Theme>('light');
const NotificationContext = createContext<Notification[]>([]);
function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<Theme>('light');
const [notifications, setNotifications] = useState<Notification[]>([]);
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<NotificationContext.Provider value={notifications}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}状态下沉
将状态尽可能地放在离使用它的组件最近的地方:
// ❌ 状态提升到了不需要它的父组件
function Page() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<Header /> {/* 不需要 isModalOpen */}
<Content /> {/* 不需要 isModalOpen */}
<Footer>
<ModalButton /> {/* 唯一需要的地方 */}
</Footer>
</div>
);
}
// ✅ 将状态封装在真正使用它的组件中
function Page() {
return (
<div>
<Header />
<Content />
<Footer>
<ModalButton /> {/* 模态框状态在此组件内部 */}
</Footer>
</div>
);
}使用 useRef 避免不必要的渲染
function SearchInput() {
const [query, setQuery] = useState('');
const debounceTimerRef = useRef<ReturnType<typeof setTimeout>>();
const latestQueryRef = useRef(query);
// 保持 ref 同步,但不触发渲染
useEffect(() => {
latestQueryRef.current = query;
}, [query]);
const handleThrottledSearch = useCallback((value: string) => {
setQuery(value); // 立即更新 input 显示
// 防抖 API 调用
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
performSearch(latestQueryRef.current);
}, 300);
}, []);
return <input value={query} onChange={(e) => handleThrottledSearch(e.target.value)} />;
}八、性能分析工具
React DevTools Profiler
React DevTools 的 Profiler 面板可以帮助定位渲染瓶颈:
- 打开 React DevTools → Profiler 选项卡
- 点击录制按钮,执行操作,停止录制
- 查看火焰图——灰色组件表示跳过了渲染,黄色/红色表示渲染耗时较长
使用 Profiler API
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update' | 'nested-update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number,
) {
if (actualDuration > 16) {
console.warn(`[${id}] 渲染耗时 ${actualDuration}ms,超过一帧预算`);
}
}
function App() {
return (
<Profiler id="Dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}为什么不能用 useMemo/useCallback 包装所有东西
// ❌ 过度优化——得不偿失
const doubled = useMemo(() => count * 2, [count]);
const handleClick = useCallback(() => setCount((c) => c + 1), []);
const title = useMemo(() => `${name}'s Profile`, [name]);
// ✅ 仅在以下情况使用这些 API:
// 1. 计算确实昂贵(复杂的数据转换、排序、过滤)
// 2. 引用稳定性对于 memo 子组件至关重要
// 3. 作为其他 Hook(useEffect、useMemo)的依赖九、优化决策流程图
在决定如何优化时,按以下优先级行动:
遇到性能问题
│
├─→ 1. 先测量(Profiler/Performance),确定瓶颈
│
├─→ 2. 是否是大量数据渲染?
│ └─→ 使用虚拟列表(react-window/react-virtuoso)
│
├─→ 3. 是否是首次加载慢?
│ └─→ 代码分割 + 懒加载(React.lazy + Suspense)
│
├─→ 4. 是否是组件频繁无意义重渲染?
│ ├─→ 状态下沉到使用它的组件
│ ├─→ Context 拆分
│ ├─→ React.memo 包裹渲染开销大的组件
│ └─→ useCallback/useMemo 稳定 props 引用
│
└─→ 5. 是否是昂贵的计算?
└─→ useMemo 缓存结果
结语
React 性能优化的核心原则是:先测量,再优化。在没有明确性能问题的前提下,盲目添加 memo、useMemo 和 useCallback 只会让代码更难维护。
真正有效的性能优化策略从架构层面开始——合理的组件拆分、状态下沉、Context 设计和代码分割——这些基础做对了,微观层面的 API 才有意义。虚拟列表和代码分割是解决特定性能瓶颈的利器,但也只在确实需要时使用。
记住:过早的优化是万恶之源,但基于测量的优化是工程美德。