前言

当 React 应用变得庞大时,性能问题逐渐浮现:不必要的重渲染、缓慢的初始加载、庞大的静态资源。本文将系统性地讲解 React 性能优化的各种策略,帮助你打造流畅的用户体验。

性能仪表盘

React 渲染机制

虚拟 DOM 的工作原理

React 使用虚拟 DOM 来减少对真实 DOM 的直接操作。每次状态变化时,React 会生成新的虚拟 DOM 树,然后与旧的虚拟 DOM 树进行 Diff 比较,最后只更新真实 DOM 中变化的部分。

1
2
3
4
5
6
7
// 简化的虚拟 DOM 节点结构
interface VNode {
type: string | Function;
props: Record<string, any>;
children: VNode[];
key: string | number | null;
}

什么触发了重渲染

React 组件在以下情况下会重新渲染:

  1. 状态更新useState 的 setter 被调用
  2. 父组件重渲染:子组件默认会跟随父组件一起渲染
  3. Context 变化:消费了某个 Context 的组件
  4. Hooks 依赖变化:使用了外部状态

关键认知:React 的重渲染并不一定意味着低性能。但如果重渲染频率过高且组件本身昂贵,就可能产生性能问题。

优化策略

React.memo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { memo, useState } from 'react';

// 普通组件 - 父组件每次重渲染都会重新渲染
function ExpensiveChild({ data }: { data: string[] }) {
console.log('Rendering ExpensiveChild');
return (
<ul>
{data.map(item => <li key={item}>{item}</li>)}
</ul>
);
}

// 使用 memo 包裹 - 只在 props 变化时重渲染
const MemoizedChild = memo(ExpensiveChild);

function Parent() {
const [count, setCount] = useState(0);
const data = ['Apple', 'Banana', 'Cherry'];

return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<MemoizedChild data={data} />
</div>
);
}

自定义比较函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const MemoizedList = memo(
function UserList({ users }: { users: User[] }) {
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
},
(prevProps, nextProps) => {
// 自定义比较:只在数组长度或第一个元素 ID 变化时才重渲染
return (
prevProps.users.length === nextProps.users.length &&
prevProps.users[0]?.id === nextProps.users[0]?.id
);
}
);

useMemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useMemo, useState } from 'react';

function Dashboard({ transactions }: { transactions: Transaction[] }) {
const [filter, setFilter] = useState('all');

// 只在 transactions 或 filter 变化时重新计算
// 避免在每次重渲染时都执行昂贵的计算
const stats = useMemo(() => {
console.log('Computing expensive stats...');
return {
total: transactions.reduce((sum, t) => sum + t.amount, 0),
count: transactions.length,
average: transactions.reduce((sum, t) => sum + t.amount, 0) / transactions.length,
maxAmount: Math.max(...transactions.map(t => t.amount)),
};
}, [transactions, filter]);

return (
<div>
<h2>Dashboard</h2>
<p>Total: {stats.total}</p>
<p>Count: {stats.count}</p>
<p>Average: {stats.average.toFixed(2)}</p>
<p>Max: {stats.maxAmount}</p>
</div>
);
}

useCallback

useCallback 返回一个记忆化的回调函数,依赖项不变时返回的是同一个函数引用。这对于传递给子组件的回调函数尤为重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useCallback, useState, memo } from 'react';

const ChildButton = memo(function ChildButton({
onClick,
label
}: {
onClick: () => void;
label: string;
}) {
console.log(`Rendering ${label}`);
return <button onClick={onClick}>{label}</button>;
});

function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);

// ✅ 正确:使用 useCallback 稳定函数引用
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);

// ❌ 错误:每次重渲染都创建新函数,导致 memo 失效
// const increment = () => setCount(c => c + 1);

return (
<div>
<ChildButton onClick={increment} label="Increment" />
<button onClick={() => setOther(o => o + 1)}>
Other: {other}
</button>
</div>
);
}

虚拟化长列表

react-window

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style} className="list-item">
<span>{index + 1}</span>
<span>{items[index]}</span>
</div>
);

return (
<List
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
}

性能对比

列表长度 普通渲染 虚拟化渲染
100 条 ~5ms ~3ms
1,000 条 ~30ms ~4ms
10,000 条 ~300ms ~5ms
100,000 条 页面卡死 ~6ms

代码分割与懒加载

React.lazy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { lazy, Suspense } from 'react';

// 懒加载组件
const Dashboard = lazy(() => import('./components/Dashboard'));
const Settings = lazy(() => import('./components/Settings'));
const Analytics = lazy(() => import('./components/Analytics'));

function App() {
return (
<Suspense fallback={<div className="loading-skeleton">Loading...</div>}>
<Router>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Router>
</Suspense>
);
}

预加载策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 鼠标悬停时预加载
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const handleMouseEnter = () => {
// 根据路由动态预加载对应组件
if (to === '/analytics') {
import('./components/Analytics');
} else if (to === '/settings') {
import('./components/Settings');
}
};

return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}

状态管理优化

细粒度状态

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 不好:一个大的对象状态
const [state, setState] = useState({
name: '',
age: 0,
address: '',
preferences: {}
});

// ✅ 更好:拆分为独立状态
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [address, setAddress] = useState('');

Context 拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 按职责拆分 Context
const ThemeContext = createContext<Theme>('light');
const UserContext = createContext<User | null>(null);
const SettingsContext = createContext<Settings>(defaultSettings);

// 组件只需订阅需要的 Context
function Header() {
const theme = useContext(ThemeContext);
// 主题变化时重渲染,但用户信息变化时不受影响
return <header className={theme}>...</header>;
}

function Profile() {
const user = useContext(UserContext);
// 用户信息变化时重渲染,但主题变化时不受影响
return <div>{user?.name}</div>;
}

图片优化

懒加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function LazyImage({ src, alt }: { src: string; alt: string }) {
const [loaded, setLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '200px' } // 提前 200px 加载
);

if (imgRef.current) {
observer.observe(imgRef.current);
}

return () => observer.disconnect();
}, []);

return (
<img
ref={imgRef}
src={loaded ? src : undefined}
alt={alt}
className="lazy-image"
/>
);
}

性能度量工具

React DevTools Profiler

React DevTools 内置的 Profiler 面板可以记录每次渲染的耗时,帮助定位性能瓶颈。

useWhyDidYouUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function useWhyDidYouUpdate(name: string, props: Record<string, any>) {
const previousProps = useRef<Record<string, any>>();

useEffect(() => {
if (previousProps.current) {
const changedProps: Record<string, [any, any]> = {};
const allKeys = Object.keys({ ...previousProps.current, ...props });

for (const key of allKeys) {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = [previousProps.current[key], props[key]];
console.log(`[${name}] "${key}" changed:`, changedProps[key]);
}
}
}
previousProps.current = props;
});
}

总结

React 性能优化是一个系统工程,按优先级排列:

  1. 代码分割:首屏只加载必需代码,最大提升 TTI
  2. 虚拟化长列表:DOM 节点数量是性能杀手
  3. React.memo + useMemo + useCallback:减少不必要的重渲染
  4. 状态拆分与 Context 分离:缩小重渲染影响范围
  5. 图片懒加载与优化:减少网络请求和内存占用

记住:「过早优化是万恶之源」。先用工具找到真正的瓶颈,再有针对性地优化。