一、React 为什么会"慢"?
React 默认行为:父组件重新渲染 → 所有子组件都跟着重新渲染,不管子组件的 props 有没有变。
App 重新渲染
├── Header 重新渲染 ← props 没变也渲染了(浪费)
├── Content 重新渲染 ← 确实需要更新
└── Footer 重新渲染 ← props 没变也渲染了(浪费)
性能优化的核心:减少不必要的渲染和计算。
二、优化手段
1. React.memo --- 跳过不必要的子组件渲染
用 memo 包裹子组件,props 不变就不重新渲染。
jsx
import { memo } from 'react';
// 没有 memo:父组件每次渲染,Header 都跟着渲染
// 有 memo:只有 title 变了才重新渲染
const Header = memo(function Header({ title }) {
console.log('Header 渲染了');
return <h1>{title}</h1>;
});
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Header title="标题" /> {/* count 变了,但 title 没变 → Header 不渲染 */}
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
);
}
memo 失效的常见原因:
jsx
// ❌ 每次渲染都创建新对象/数组/函数 → 引用变了 → memo 失效
<Child style={{ color: 'red' }} /> // 每次都是新对象
<Child list={[1, 2, 3]} /> // 每次都是新数组
<Child onClick={() => doSomething()} /> // 每次都是新函数
// ✅ 用 useMemo / useCallback 稳定引用
const style = useMemo(() => ({ color: 'red' }), []);
const list = useMemo(() => [1, 2, 3], []);
const handleClick = useCallback(() => doSomething(), []);
2. useMemo --- 缓存计算结果
jsx
function ProductList({ products, keyword }) {
// ❌ 每次渲染都重新过滤(即使 products 和 keyword 没变)
// const filtered = products.filter(p => p.name.includes(keyword));
// ✅ 只在依赖变化时才重新计算
const filtered = useMemo(() => {
return products.filter(p => p.name.includes(keyword));
}, [products, keyword]);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
3. useCallback --- 缓存函数引用
jsx
function Parent() {
const [count, setCount] = useState(0);
// ✅ 函数引用稳定,配合 memo 的子组件不会重新渲染
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <MemoChild onClick={handleClick} />;
}
const MemoChild = memo(({ onClick }) => {
console.log('子组件渲染');
return <button onClick={onClick}>点击</button>;
});
4. 列表优化 --- 正确使用 key
jsx
// ❌ 用 index 做 key(增删时性能差,可能出 bug)
list.map((item, i) => <Item key={i} data={item} />)
// ✅ 用唯一 id 做 key
list.map(item => <Item key={item.id} data={item} />)
5. 懒加载 --- React.lazy + Suspense
按需加载组件,首屏不需要的组件延后加载。
jsx
import { lazy, Suspense } from 'react';
// 懒加载:只有用到时才下载这个组件的代码
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
6. 避免不必要的 state
jsx
// ❌ 能从现有 state 算出来的值,不要单独存 state
const [list, setList] = useState([]);
const [count, setCount] = useState(0); // 多余!
// ✅ 直接计算
const [list, setList] = useState([]);
const count = list.length; // 不需要额外的 state 和 setState
7. 状态下放 --- 缩小渲染范围
jsx
// ❌ 输入框的 state 放在顶层 → 每次输入整个页面都重新渲染
function App() {
const [text, setText] = useState('');
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<HeavyComponent /> {/* 每次输入都跟着渲染 */}
</div>
);
}
// ✅ 把输入框抽成独立组件 → 只有输入框自己重新渲染
function SearchInput() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
function App() {
return (
<div>
<SearchInput />
<HeavyComponent /> {/* 不受输入影响 */}
</div>
);
}
8. 虚拟列表 --- 长列表优化
10000 条数据不用全部渲染,只渲染可视区域内的几十条。
bash
npm install 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={400} // 可视区域高度
width="100%"
itemCount={items.length} // 总数据量
itemSize={50} // 每行高度
>
{Row}
</FixedSizeList>
);
}
// 10000 条数据,实际只渲染 ~10 个 DOM 节点
三、优化手段速查表
| 优化方式 | 解决什么问题 | 使用场景 |
|---|---|---|
React.memo |
子组件不必要的重新渲染 | props 不常变的子组件 |
useMemo |
重复的昂贵计算 | 过滤、排序、格式化 |
useCallback |
函数引用变化导致子组件渲染 | 配合 memo 使用 |
React.lazy |
首屏加载太慢 | 路由级别懒加载 |
| 状态下放 | state 放太高导致大范围渲染 | 局部交互(输入框、弹窗) |
| 虚拟列表 | 长列表 DOM 太多 | 数据量 > 几百条 |
| 正确的 key | 列表 Diff 效率低 | 所有列表渲染 |
| 避免内联对象/函数 | memo 失效 | 传给 memo 组件的 props |
四、高频面试题
Q1:React.memo、useMemo、useCallback 的区别?
| 对比 | React.memo | useMemo | useCallback |
|---|---|---|---|
| 作用对象 | 组件 | 值 | 函数 |
| 做什么 | props 不变就跳过渲染 | 缓存计算结果 | 缓存函数引用 |
| 返回 | 新组件 | 缓存的值 | 缓存的函数 |
Q2:什么时候不该用 memo/useMemo?
- 组件本身很轻量,渲染很快 → 加 memo 的对比开销反而更大
- props 每次都会变 → memo 每次对比完还是要渲染,白对比了
- 计算不复杂 → useMemo 的缓存开销 > 重新计算的开销
原则:先写正确的代码,遇到性能问题再优化,不要过早优化。
Q3:如何排查 React 性能问题?
- React DevTools Profiler --- 录制渲染,看哪些组件渲染了、耗时多久
- React.StrictMode --- 开发模式下会故意双重渲染,帮你发现副作用问题
- Chrome Performance --- 录制火焰图,看 JS 执行耗时
- why-did-you-render --- 第三方库,自动提示不必要的渲染
Q4:React 18 有哪些性能相关的新特性?
| 特性 | 说明 |
|---|---|
| 自动批处理 | 所有 setState 自动合并,减少渲染次数 |
| useTransition | 标记低优先级更新,不阻塞用户交互 |
| useDeferredValue | 延迟更新某个值,类似防抖 |
| Suspense 改进 | 配合 lazy 和数据请求,更好的加载体验 |
jsx
import { useTransition } from 'react';
function Search() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setKeyword(e.target.value); // 高优先级:立即更新输入框
// 低优先级:搜索结果可以稍后更新,不阻塞输入
startTransition(() => {
setResults(filterData(e.target.value));
});
};
return (
<div>
<input value={keyword} onChange={handleChange} />
{isPending ? <p>搜索中...</p> : <ResultList data={results} />}
</div>
);
}