一、React 性能问题的本质
React 的性能问题几乎都指向同一个根源:不必要的重新渲染。当组件的 state、props 或 context 变化时,React 会重新调用该组件及其所有子组件的 render 函数(或函数体),即使子组件的 props 没有变化。
1 2 3 4 5 6 7 8 9 10 11 12
| 父组件状态更新 │ ├── 父组件重新渲染 │ ├── 子组件 A(props 没变)→ ❌ 不必要地重渲染 │ ├── 子组件 B(props 变了)→ ✅ 需要重渲染 │ └── 子组件 C(props 没变)→ ❌ 不必要地重渲染 │ └── 优化手段 ├── React.memo → 阻止 props 未变时的重渲染 ├── useMemo → 缓存计算结果,避免无效计算 ├── useCallback → 稳定函数引用,配合 memo 生效 └── 状态下移 → 减少变动的范围
|
二、优化手段详解
React.memo——防止 props 未变时的重渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Child({ name }) { return <div>{name}</div>; }
const MemoChild = React.memo(function Child({ name }) { return <div>{name}</div>; });
const MemoChild = React.memo(Child, (prevProps, nextProps) => { return prevProps.name === nextProps.name; });
|
1 2 3 4 5
| 使用原则: ├── 组件接收 props 且渲染次数频繁 → memo ├── 组件本身渲染很重(大列表、图表)→ memo ├── 组件 props 基本不变 → memo └── 组件几乎每次都重新传入新 props → memo 无效,需配合 useCallback
|
useMemo——缓存计算结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function Dashboard({ data, filter }) { const filtered = data.filter(d => d.type === filter) .sort((a, b) => b.value - a.value); return <Chart data={filtered} />; }
function Dashboard({ data, filter }) { const filtered = useMemo(() => { return data.filter(d => d.type === filter) .sort((a, b) => b.value - a.value); }, [data, filter]); return <Chart data={filtered} />; }
|
1 2 3 4 5
| 适用场景: ├── 大数组的 map/filter/sort 操作 ├── 深拷贝或复杂对象转换 ├── React.createElement 结果 └── 需要引用的稳定对象(配合 memo)
|
useCallback——稳定函数引用
1 2 3 4 5 6 7 8 9 10 11
| function Parent() { const handleClick = () => console.log('clicked'); return <MemoButton onClick={handleClick} />; }
function Parent() { const handleClick = useCallback(() => console.log('clicked'), []); return <MemoButton onClick={handleClick} />; }
|
1 2 3 4 5
| 使用原则: ├── 传给被 React.memo 包裹的子组件 props → useCallback ├── 作为 useEffect 的依赖 → useCallback ├── 自定义 Hook 返回的函数 → useCallback └── 简单内联函数(如 onChange)且无 memo 子组件 → 不需要
|
useMemo 和 useCallback 的误区
1 2 3 4 5 6 7 8 9 10 11
| ❌ 不要把所有变量都用 useMemo 包裹 → 缓存本身有内存和比较开销 → 只对昂贵的计算或引用稳定性有意义
❌ 不要用 useCallback 包裹所有函数 → 每次渲染依然创建函数,只是多了个 useCallback 调用 → 只有配合 memo 子组件时才有意义
❌ 不要用 useMemo 做"提前优化" → 先测量再优化 → 简单的计算(小于 1000 项的 map)不需要 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 28 29
| function Page() { const [count, setCount] = useState(0); return ( <div> <Counter count={count} setCount={setCount} /> <Header /> <Sidebar /> <Footer /> </div> ); }
function Page() { return ( <div> <CounterBlock /> <Header /> <Sidebar /> <Footer /> </div> ); }
function CounterBlock() { const [count, setCount] = useState(0); return <Counter count={count} setCount={setCount} />; }
|
Context 优化
1 2 3 4 5 6 7 8 9 10 11 12 13
| <ThemeContext.Provider value={{ theme, setTheme }}>
function App() { const [theme, setTheme] = useState('light'); const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> <Content /> </ThemeContext.Provider> ); }
|
三、懒加载与代码分割
路由级代码分割
1 2 3 4 5 6 7 8 9
| const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() { return ( <Suspense fallback={<Loading />}> <Route path="/dashboard" element={<Dashboard />} /> </Suspense> ); }
|
组件级按需加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Editor() { const [showChart, setShowChart] = useState(false); return ( <div> <button onClick={() => setShowChart(true)}>显示图表</button> {showChart && ( <Suspense fallback={<div>加载中...</div>}> <HeavyChart /> </Suspense> )} </div> ); } const HeavyChart = lazy(() => import('./HeavyChart'));
|
四、排查手段
1 2 3 4 5 6 7 8 9 10 11
| 操作步骤: 1. 安装 React DevTools 浏览器扩展 2. 打开 Profiler 面板,点击录制按钮 3. 执行需要分析的操作 4. 停止录制,查看火焰图
排查问题: ├── 灰色柱状条 → 应该跳过但没跳过 → 检查 React.memo ├── 渲染耗时长的组件 → useMemo 缓存数据 ├── 不应该渲染的组件高亮了 → props 未稳定或 context 变化 └── 渲染次数过多的组件 → 状态提升过高
|
Highlight Updates
1 2 3 4 5 6 7 8 9
| 操作步骤: 1. React DevTools → Settings → Highlight updates 2. 在页面中交互 3. 观察绿/蓝色闪烁范围
排查问题: ├── 闪烁范围过大 → 不必要的重渲染 ├── 闪烁频率过高 → 状态变化触发过多渲染 └── 应该闪烁的没闪烁 → props 或 state 未正确传递
|
Why Did You Render
1
| npm install @welldone-software/why-did-you-render
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React from 'react';
if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); whyDidYouRender(React, { trackAllPureComponents: true, trackHooks: true, }); }
const MyComponent = React.memo(function MyComponent(props) { return <div>{props.name}</div>; }); MyComponent.whyDidYouRender = true;
|
1 2 3 4 5 6
| 输出示例: "WhyDidYouRender - MyComponent Props Diff: - name: 'Alice' → 'Alice' (same value, different reference)"
→ name 值相同但引用不同 → 需要 useCallback/useMemo
|
五、排查清单
| 现象 | 排查手段 | 常用方案 |
|---|
| 输入框输入卡顿 | Profiler 火焰图 | 状态下移、React.memo |
| 列表重排序卡顿 | Highlight Updates | 唯一 key、虚拟滚动 |
| 弹窗打开慢 | Network 瀑布图 | React.lazy + 代码分割 |
| 父更新子组件全重渲染 | Why Did You Render | React.memo + useCallback |
| Context 变化大面积重渲染 | Profiler | 拆分 Context、useMemo value |
| 图表渲染慢 | flame graph | useMemo 缓存数据 |
| 页面切换白屏 | Network 图 | Suspense + 路由级分割 |