React 性能优化完全指南
更新: 10/16/2025 字数: 0 字 时长: 0 分钟
概述
性能优化是 React 应用开发的重要环节。本文将深入讲解 React 性能优化的各个方面,从原理到实践,帮助你构建高性能的 React 应用。
性能问题的根源
React 的渲染流程
触发更新 → Render 阶段 → Commit 阶段 → 浏览器渲染性能问题通常出现在:
- 不必要的重新渲染:组件没有变化,但仍然重新渲染
- 昂贵的计算:每次渲染都执行复杂计算
- 大列表渲染:渲染成千上万个列表项
- 频繁的 DOM 操作:过多的 DOM 更新
React.memo - 避免不必要的渲染
基本用法
jsx
// 未优化的组件
function UserCard({ user }) {
console.log('UserCard 渲染');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// 使用 React.memo 优化
const UserCard = React.memo(function UserCard({ user }) {
console.log('UserCard 渲染');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
// 父组件
function UserList() {
const [count, setCount] = useState(0);
const user = { name: 'Alice', email: 'alice@example.com' };
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
点击次数: {count}
</button>
{/* 使用 memo 后,user 没变化时不会重新渲染 */}
<UserCard user={user} />
</div>
);
}React.memo 的原理
javascript
function memo(Component, arePropsEqual) {
return function MemoComponent(props) {
// 获取上一次的 props
const prevProps = usePrevious(props);
// 比较 props 是否相等
if (prevProps && arePropsEqual
? arePropsEqual(prevProps, props)
: shallowEqual(prevProps, props)
) {
// Props 相等,复用上次的结果
return prevRenderedElement;
}
// Props 不等,重新渲染
const nextElement = Component(props);
prevRenderedElement = nextElement;
return nextElement;
};
}
// 浅比较
function shallowEqual(objA, objB) {
if (Object.is(objA, objB)) return true;
if (
typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (
!Object.prototype.hasOwnProperty.call(objB, key) ||
!Object.is(objA[key], objB[key])
) {
return false;
}
}
return true;
}自定义比较函数
jsx
const UserCard = React.memo(
function UserCard({ user, onEdit }) {
return (
<div>
<h3>{user.name}</h3>
<button onClick={onEdit}>编辑</button>
</div>
);
},
// 自定义比较:只比较 user.id
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
}
);常见陷阱
jsx
function Parent() {
const [count, setCount] = useState(0);
// ❌ 错误:每次都创建新对象
return <Child user={{ name: 'Alice' }} />;
// ❌ 错误:每次都创建新函数
return <Child onClick={() => console.log('click')} />;
// ❌ 错误:每次都创建新数组
return <Child items={[1, 2, 3]} />;
}
const Child = React.memo(function Child(props) {
// 因为 props 总是新的,memo 失效
return <div>{/* ... */}</div>;
});
// ✅ 正确:提取到组件外部
const user = { name: 'Alice' };
const items = [1, 2, 3];
function Parent() {
const [count, setCount] = useState(0);
// 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log('click');
}, []);
return (
<Child
user={user}
items={items}
onClick={handleClick}
/>
);
}useMemo - 缓存计算结果
基本用法
jsx
function ExpensiveComponent({ items, filter }) {
// ❌ 不好:每次渲染都会执行
const filteredItems = items.filter(item =>
item.category === filter
);
// ✅ 好:只在 items 或 filter 变化时才重新计算
const filteredItems = useMemo(() => {
console.log('计算过滤结果');
return items.filter(item => item.category === filter);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}何时使用 useMemo
jsx
// ✅ 适合使用 useMemo 的场景:
// 1. 昂贵的计算
function DataAnalysis({ data }) {
const analysis = useMemo(() => {
// 复杂的数据分析
return performComplexAnalysis(data);
}, [data]);
return <Chart data={analysis} />;
}
// 2. 防止子组件重新渲染
function Parent() {
const [count, setCount] = useState(0);
// 缓存对象,避免 Child 重新渲染
const config = useMemo(() => ({
color: 'blue',
size: 'large'
}), []);
return <Child config={config} />;
}
// 3. 作为其他 Hook 的依赖
function useFilteredData(items, filter) {
const filteredItems = useMemo(
() => items.filter(item => item.category === filter),
[items, filter]
);
useEffect(() => {
// filteredItems 稳定,不会导致 effect 频繁执行
saveToLocalStorage(filteredItems);
}, [filteredItems]);
return filteredItems;
}
// ❌ 不适合使用 useMemo 的场景:
// 1. 简单的计算
function Component({ a, b }) {
// 不需要 memo,计算很简单
const sum = a + b;
return <div>{sum}</div>;
}
// 2. 原始值
function Component({ name }) {
// 不需要 memo,字符串本身就是值
const uppercaseName = useMemo(() => name.toUpperCase(), [name]);
return <div>{uppercaseName}</div>;
}useMemo 的实现原理
javascript
function useMemo(create, deps) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
// 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变化,返回缓存的值
return prevState[0];
}
}
}
// 依赖变化,重新计算
const nextValue = create();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}useCallback - 缓存函数
基本用法
jsx
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');
// ❌ 不好:每次渲染都创建新函数
const handleToggle = (id) => {
toggleTodo(id);
};
// ✅ 好:缓存函数
const handleToggle = useCallback((id) => {
toggleTodo(id);
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<div onClick={() => onToggle(todo.id)}>
{todo.text}
</div>
);
});useCallback vs useMemo
jsx
// useCallback
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// 等价于
const handleClick = useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);
// useCallback 就是返回函数的 useMemo 的语法糖实际应用场景
jsx
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// 防抖搜索
const debouncedSearch = useCallback(
debounce((searchTerm) => {
fetchResults(searchTerm).then(setResults);
}, 300),
[] // 只创建一次
);
const handleChange = useCallback((e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
}, [debouncedSearch]);
return (
<div>
<input value={query} onChange={handleChange} />
<ResultList results={results} />
</div>
);
}
// 自定义 debounce Hook
function useDebounce(callback, delay) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
return useMemo(() => {
return debounce((...args) => {
callbackRef.current(...args);
}, delay);
}, [delay]);
}列表优化
虚拟列表(React Window)
jsx
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
// 渲染单个列表项
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600} // 列表高度
itemCount={items.length} // 总项数
itemSize={50} // 每项高度
width="100%"
>
{Row}
</FixedSizeList>
);
}
// 处理 10000 项,只渲染可见的部分
function App() {
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
return <VirtualList items={items} />;
}动态高度虚拟列表
jsx
import { VariableSizeList } from 'react-window';
function DynamicVirtualList({ items }) {
const listRef = useRef();
const rowHeights = useRef({});
// 获取行高
const getItemSize = (index) => {
return rowHeights.current[index] || 50;
};
// 设置行高
const setRowHeight = (index, size) => {
listRef.current.resetAfterIndex(index);
rowHeights.current[index] = size;
};
const Row = ({ index, style }) => {
const rowRef = useRef();
useEffect(() => {
if (rowRef.current) {
setRowHeight(index, rowRef.current.clientHeight);
}
}, [index]);
return (
<div ref={rowRef} style={style}>
<div style={{ padding: 10 }}>
{items[index].content}
</div>
</div>
);
};
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}分页和无限滚动
jsx
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [loading, setLoading] = useState(false);
const loadMore = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
const newItems = await fetchItems(page);
setItems(prev => [...prev, ...newItems]);
setPage(p => p + 1);
setHasMore(newItems.length > 0);
setLoading(false);
}, [page, loading, hasMore]);
// 使用 Intersection Observer
const observerTarget = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 1 }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loadMore]);
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
{hasMore && (
<div ref={observerTarget}>
{loading ? '加载中...' : '加载更多'}
</div>
)}
</div>
);
}代码分割和懒加载
React.lazy 和 Suspense
jsx
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}
// 预加载
function Navigation() {
const handleMouseEnter = () => {
// 鼠标悬停时预加载
import('./Dashboard');
};
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={handleMouseEnter}
>
Dashboard
</Link>
</nav>
);
}路由级别的代码分割
jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 按路由分割
const routes = [
{
path: '/',
component: lazy(() => import('./pages/Home')),
},
{
path: '/about',
component: lazy(() => import('./pages/About')),
},
{
path: '/products',
component: lazy(() => import('./pages/Products')),
},
];
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
{routes.map(({ path, component: Component }) => (
<Route
key={path}
path={path}
element={<Component />}
/>
))}
</Routes>
</Suspense>
</BrowserRouter>
);
}组件级别的代码分割
jsx
function ProductDetails({ productId }) {
const [showReviews, setShowReviews] = useState(false);
// 只在需要时才加载 Reviews 组件
const Reviews = lazy(() => import('./Reviews'));
return (
<div>
<ProductInfo id={productId} />
<button onClick={() => setShowReviews(true)}>
查看评论
</button>
{showReviews && (
<Suspense fallback={<Spinner />}>
<Reviews productId={productId} />
</Suspense>
)}
</div>
);
}状态管理优化
状态分割
jsx
// ❌ 不好:所有状态在一起
function App() {
const [state, setState] = useState({
user: null,
posts: [],
comments: [],
ui: { loading: false, modal: false }
});
// 任何状态变化都会导致整个组件重新渲染
}
// ✅ 好:分割状态
function App() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
// 每个状态独立变化
}状态下放
jsx
// ❌ 不好:状态在顶层
function App() {
const [selectedId, setSelectedId] = useState(null);
return (
<div>
<Sidebar />
<MainContent selectedId={selectedId} />
{/* 修改 selectedId 会导致 Sidebar 也重新渲染 */}
</div>
);
}
// ✅ 好:状态下放到使用它的组件
function App() {
return (
<div>
<Sidebar />
<MainContent />
</div>
);
}
function MainContent() {
const [selectedId, setSelectedId] = useState(null);
// 状态只影响 MainContent
return <div>{/* ... */}</div>;
}使用 Context 的性能陷阱
jsx
// ❌ 不好:Context 变化导致所有消费者重新渲染
const AppContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
<Component />
</AppContext.Provider>
);
}
// ✅ 好:分割 Context
const UserContext = createContext();
const ThemeContext = createContext();
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const userValue = useMemo(() => ({ user, setUser }), [user]);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<Component />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// 或者使用状态管理库(Zustand、Jotai)
import create from 'zustand';
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
const useThemeStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}));渲染优化模式
组件组合
jsx
// ❌ 不好:ExpensiveTree 会随 count 变化而重新渲染
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveTree />
</div>
);
}
// ✅ 好:使用组合,ExpensiveTree 不会重新渲染
function App() {
return (
<ColorPicker>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{children}
</div>
);
}提取为独立组件
jsx
// ❌ 不好
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
<ExpensiveComponent />
</div>
);
}
// ✅ 好
function Parent() {
return (
<div>
<Counter />
<ExpensiveComponent />
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
);
}性能分析工具
React DevTools Profiler
jsx
import { Profiler } from 'react';
function App() {
const onRenderCallback = (
id, // 组件的 id
phase, // "mount" 或 "update"
actualDuration, // 本次更新花费的时间
baseDuration, // 不使用 memo 的情况下的渲染时间
startTime, // 开始渲染的时间
commitTime, // 提交更新的时间
interactions // 本次更新的 interactions
) => {
console.log(`${id} (${phase}) 渲染耗时: ${actualDuration}ms`);
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}自定义性能监控 Hook
jsx
function useRenderCount(componentName) {
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
console.log(`${componentName} 渲染次数: ${renderCount.current}`);
});
}
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}
// 使用
function MyComponent(props) {
useRenderCount('MyComponent');
useWhyDidYouUpdate('MyComponent', props);
return <div>{/* ... */}</div>;
}打包优化
Bundle 分析
bash
# 使用 webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# 或使用 rollup-plugin-visualizer (Vite)
npm install --save-dev rollup-plugin-visualizerjavascript
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
]
}Tree Shaking
jsx
// ❌ 不好:导入整个库
import _ from 'lodash';
_.debounce(fn, 300);
// ✅ 好:只导入需要的部分
import debounce from 'lodash/debounce';
debounce(fn, 300);
// 或使用 ES modules 版本
import { debounce } from 'lodash-es';性能优化检查清单
🎯 渲染优化
📦 代码分割
📋 列表优化
🏪 状态管理
📊 性能监控
🌐 网络优化
总结
性能优化是一个持续的过程,关键点:
- 测量优先:先测量,再优化,避免过早优化
- 找到瓶颈:使用 Profiler 找到真正的性能瓶颈
- 合理使用:不要滥用 memo/useMemo/useCallback
- 权衡取舍:优化是有成本的,要权衡收益
记住:过早优化是万恶之源。只有在确实存在性能问题时,才进行针对性优化。