摘要 :React 性能优化的核心目标是减少不必要的 re-render(重渲染)和降低首屏/交互耗时 。优化手段分三层:组件层(memo/PureComponent/useMemo/useCallback 控制更新粒度)、加载层(lazy/Suspense 分 chunk + 虚拟列表 + Web Worker)和工程层(打包构建/资源压缩/缓存策略)。一句话概括------能不渲染就不渲染,能晚加载就晚加载,能放后台就放后台。
一、为什么需要性能优化?
React 默认的渲染机制:父组件 state 变化 → 所有子组件都会重新执行 render 函数,即使子组件的 props 没有变化。
jsx
// 父组件状态变了 → ChildA 和 ChildB 都会重新 render
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ChildA /> {/* count 变了,ChildA 也重新 render */}
<ChildB data="固定值" /> {/* props 没变,但仍然重新 render */}
</div>
);
}
当组件树变大时,无意义的 re-render 会造成明显的卡顿。优化的本质就是精准控制"谁该重新渲染"。
二、减少不必要的 Re-render
2.1 React.memo(函数组件)& PureComponent(类组件)
两者都是通过浅比较(shallow compare) props 来决定是否跳过渲染。
jsx
// 函数组件:用 React.memo 包裹
const ExpensiveComponent = React.memo(function ({ data }) {
console.log('ExpensiveComponent 渲染了');
return <div>{data.name}</div>;
});
// 只有 data 引用变了才会重新渲染
// data.name 改了但引用没变?不会触发!注意这个坑
jsx
// 类组件:继承 React.PureComponent
class ExpensiveList extends React.PureComponent {
render() {
console.log('ExpensiveList 渲染了');
return <ul>{this.props.items.map(...)}</ul>;
}
}
// 等价于 shouldComponentUpdate 做浅比较
2.2 useMemo --- 缓存计算结果
避免每次 render 都重复执行昂贵的计算。
jsx
function FilteredList({ items, filter }) {
// ❌ 每次 render 都重新过滤
const filtered = items.filter(item => item.includes(filter));
// ✅ 只有 items 或 filter 变了才重新计算
const filtered = useMemo(
() => items.filter(item => item.includes(filter)),
[items, filter] // 依赖数组
);
return <ul>{filtered.map(...)}</ul>;
}
2.3 useCallback --- 缓存函数引用
防止因父组件 re-render 导致子组件收到新的函数引用,从而破坏 memo 的效果。
jsx
function Parent({ count }) {
// ❌ 每次 render 都创建新函数 → 子组件 memo 失效
const handleClick = () => console.log(count);
// ✅ count 不变时返回同一个函数引用
const handleClick = useCallback(
() => console.log(count),
[count]
);
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => (
<button onClick={onClick}>click</button>
));
2.4 不可变数据结构 & Immer
React 的浅比较要求数据引用不变才视为"没改"。直接修改对象/数组会绕过检测。
js
// ❌ 直接修改 ------ 引用没变,React 检测不到变化
state.items.push(newItem);
setState(state); // 不会触发 re-render!
// ✅ 创建新引用
setState({
...state,
items: [...state.items, newItem]
});
Immer 用 Proxy 拦截写操作,让你用"直接修改"的语法写出不可变更新:
js
import { produce } from 'immer';
const nextState = produce(state, draft => {
draft.items.push(newItem); // 看起来像直接修改
draft.user.name = 'newName'; // 实际 Immer 生成了新对象
});
setState(nextState);
三、按需加载与代码分割
3.1 React.lazy + Suspense
把不需要首屏立即展示的组件拆成独立 chunk,用到时再加载。
jsx
import { lazy, Suspense } from 'react';
// 动态 import → webpack 自动拆分成独立 chunk
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
{isAdmin && <AdminPanel />}
<HeavyChart />
</Suspense>
);
}
效果 :首屏 JS 体积大幅减小,HeavyChart 和 AdminPanel 的代码在需要时才下载执行。
3.2 虚拟列表(大列表优化)
渲染 10000 条 DOM 必然卡顿。虚拟列表只渲染可视区域内的几十条元素。
jsx
// react-window 或 react-virtualized
import { FixedSizeList as List } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>{items[index].name}</div>
);
return (
<List
height={500} // 容器高度
itemCount={10000} // 总条数
itemSize={50} // 每行高度
>
{Row}
</List>
);
}
// 无论 10000 条还是 100 万条,DOM 节点始终只有 ~11 个(500/50 + 缓冲)
四、Web Worker --- 把重计算移出主线程
JS 是单线程的,复杂计算会阻塞 UI 渲染。Web Worker 在独立线程中运行,不阻塞主线程。
js
// worker.js
self.onmessage = function(e) {
const { data } = e.data;
// 耗时计算:大数据排序、图像处理、复杂算法
const result = heavyComputation(data);
self.postMessage(result);
};
// 组件中使用
function DataProcessor({ rawData }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker('./worker.js');
worker.onmessage = (e) => {
setResult(e.data); // 计算完成,拿到结果
worker.terminate();
};
worker.postMessage({ data: rawData }); // 发送数据给 worker
return () => worker.terminate();
}, [rawData]);
return result ? <ResultView data={result} /> : <Spinner />;
}
五、状态管理优化
| 方案 | 适用场景 | 优化要点 |
|---|---|---|
| useState / useReducer | 组件内或简单跨组件 | 拆分 state,避免无关 state 变化触发大面积 re-render |
| Context + useReducer | 中小型应用全局状态 | 拆分多个 Context,不要把所有状态塞进一个 Provider |
| Redux / Zustand / Jotai | 大型应用 | 用 selector 精准订阅,只监听需要的 slice |
| 外部状态库(SWR/React Query) | 服务端数据 | 内置缓存、去重、自动重请求 |
关键原则------Context 的粒度要细:
jsx
// ❌ 一个大 Context → 任何状态变化都导致所有消费者 re-render
const AppContext = createContext({ user, theme, locale, cart });
// ✅ 拆成多个小 Context → 各管各的
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
// 只消费 theme 的组件不会因为 user 变化而 re-render
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button style={{ color: theme.primary }}>click</button>;
}
六、工程层优化全景
你的手绘笔记覆盖了从构建到首屏的全链路:
| 层级 | 手段 | 效果 |
|---|---|---|
| 打包构建 | Terser 压缩、Code Splitting(chunk)、Tree Shaking、构建缓存 | 减少包体积、加快构建速度 |
| 应用层面 | CSS transform 替代 top/left(避免 reflow)、Web Worker | 减少主线程阻塞 |
| 资源加载 | Gzip/Brotli 压缩、字体子集化(font-spider)、CDN/OSS、HTTP/2 多路复用 | 加速资源传输 |
| 缓存策略 | 强缓存(Cache-Control)、协商缓存(ETag)、策略缓存(Service Worker) | 减少重复请求 |
| 首屏资源 | Next.js SSR/SSG、<link rel="prefetch">、首屏关键路径优先 |
加快 FCP/LCP |
七、完整流程串联:一次 re-render 的生命周期
scss
用户操作 / 定时器 / 网络响应
│
▼
setState / dispatch
│
▼
┌─────────────────────┐
│ 1. 标记组件为 dirty │
└──────────┬──────────┘
│
▼
┌─────────────────────┐ 未命中 memo
│ 2. shouldComponentUpdate│ ──→ 执行 render
│ 或 React.memo 比较 │
└──────────┬──────────┘
│ 命中 memo(props 浅比较相等)
▼
⏭️ 跳过 render ✅
│
▼ (未命中)
┌─────────────────────┐
│ 3. 执行 render 函数 │ ← 这里调用 useMemo/useCallback
│ 生成新的 VNode │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 4. Reconciliation │ ← Diff 算法对比新旧 VNode
│ (Diffing) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ 5. 提交变更到 DOM │ ← commitRoot,批量更新
│ (Commit) │
└──────────┬──────────┘
│
▼
浏览器绘制 (Paint)
优化插入点:
- 第 2 步 →
React.memo/PureComponent/shouldComponentUpdate - 第 3 步 →
useMemo缓存计算、useCallback缓存函数 - 第 4 步 →
key属性帮助 Diff 复用节点、虚拟列表减少节点数 - 第 5 步 → CSS transform 触发 GPU 合成层,减少 layout/paint
八、各方案选型对比
| 场景 | 推荐方案 | 不推荐 |
|---|---|---|
| 纯展示组件,props 很少变 | React.memo |
无脑全量 re-render |
| 传递给子组件的回调函数 | useCallback + memo |
每次render创建新函数 |
| 渲染前需要昂贵计算 | useMemo |
直接在 render 里算 |
| 大数据列表(1000+ 条) | 虚拟列表(react-window) | 全部渲染 |
| 页面中有重型非首屏组件 | React.lazy + Suspense |
同步 import |
| 复杂计算/数据处理 | Web Worker | 主线程同步跑 |
| 全局状态频繁更新 | 拆分多个 Context / Zustand | 单一大 Context |
| 对象/数组深层更新 | Immer | 手动展开运算符(易出错) |
九、记忆口诀
- memo 控入口:React.memo 守大门,props 没变别进来
- Memo 算得快:useMemo 存结果,useCallback 存函数
- 懒加载减体积:lazy + Suspense,chunk 按需下
- 大表虚拟化:一万条只画十条,react-window 来帮忙
- Worker 搬后台:重活丢给 Worker,主线程不卡顿
- 一句话总结 :能 memo 就 memo,能拆就拆,能懒就懒,能搬就搬