主题
Day 11:useCallback + React.memo 性能优化
今天完成什么
- 理解 React 的重渲染机制
- 用
React.memo包裹子组件,避免无效渲染 - 用
useCallback稳定回调函数引用
接在昨天哪里
昨天提取了自定义 Hook,代码更简洁了。
但当前每次 config 变化,所有子组件都会重新渲染——即使那个子组件的 props 根本没变。
今天优化这个问题。
概念解释
React 的重渲染机制
React 重新渲染的触发条件:
- 组件自己的 state 变了
- 父组件重新渲染了
text
LowCodeConfigPage(config 变了,重新渲染)
↓ 所有子组件都重新渲染
├─ ComponentSidebar(没用到 config,也重渲染了)
├─ ConfigPanel
└─ PreviewPanelComponentSidebar 没用 config,但父组件重渲染,它也跟着跑了一遍。
React.memo
用 React.memo 包裹组件后,只有 props 真正变化时才重新渲染:
tsx
const ComponentSidebar = React.memo(function ComponentSidebar() {
return <div>...</div>;
});或者导出时包裹:
tsx
function ComponentSidebar() { ... }
export default React.memo(ComponentSidebar);React.memo 如何比较 props
默认用 浅比较(shallow equal):
- 基本类型(string、number、boolean):值相等就认为没变
- 对象 / 函数:引用相等才认为没变
这里有个坑:
tsx
function LowCodeConfigPage() {
// ❌ 每次渲染都是新函数,引用不同
const handleChange = (config) => setConfig(config);
return <ConfigPanel onChange={handleChange} />;
}即使 ConfigPanel 用了 React.memo,因为 handleChange 每次都是新函数,props 比较时引用不同,还是会重新渲染。
useCallback
useCallback 缓存函数,只在依赖变化时创建新函数:
tsx
const handleChange = useCallback((config) => {
setConfig(config);
}, []); // 空数组:这个函数永远不变这样 handleChange 引用稳定,React.memo 才真正有效。
什么时候用
- 大型组件树(几十上百个组件):有必要
- 小项目(当前这个配置页):提升不大,但是必须掌握的概念
本章是学习用途,实际工作中要先 profile 再优化,不要一上来就到处加 memo。
动手实现
第 1 步:用 React.memo 包裹不需要频繁重渲的组件
修改 src/components/ComponentSidebar.tsx:
tsx
import { memo } from 'react';
function ComponentSidebar() {
return (
<div style={{
flex: '0 0 200px',
backgroundColor: '#f5f5f5',
padding: '20px',
borderRight: '1px solid #ddd'
}}>
<h3>组件列表</h3>
<div style={{
padding: '10px',
backgroundColor: '#fff',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer'
}}>
InfoCard
</div>
</div>
);
}
export default memo(ComponentSidebar);第 2 步:用 useCallback 稳定 onChange 回调
修改 src/components/LowCodeConfigPage.tsx:
tsx
import { useState, useCallback } from 'react';
import ComponentSidebar from './ComponentSidebar';
import ConfigPanel from './ConfigPanel';
import PreviewPanel from './PreviewPanel';
import { InfoCardConfig, initialInfoCardConfig } from '../types/config';
function LowCodeConfigPage() {
const [config, setConfig] = useState<InfoCardConfig>(initialInfoCardConfig);
// useCallback 保证 handleChange 引用稳定
const handleChange = useCallback((newConfig: InfoCardConfig) => {
setConfig(newConfig);
}, []); // 没有依赖,永远不变
return (
<div style={{
display: 'flex',
height: '100vh',
fontFamily: 'sans-serif'
}}>
<ComponentSidebar />
<ConfigPanel config={config} onChange={handleChange} />
<PreviewPanel config={config} />
</div>
);
}
export default LowCodeConfigPage;第 3 步:验证优化效果(用 React DevTools)
安装 React DevTools 浏览器扩展,打开「Profiler」面板,操作配置,观察哪些组件在重渲染。
优化前:每次输入,所有组件都渲染。
优化后:ComponentSidebar 不再跟着渲染。
什么时候用 useCallback 的简化版
如果只是 setConfig 的直接包裹,其实可以更简单:
tsx
// 直接传 setConfig 也行,因为 setConfig 引用本来就是稳定的
<ConfigPanel config={config} onChange={setConfig} />React 的 setXxx 函数引用是稳定的,不需要额外包 useCallback。
本章的例子是为了演示 useCallback 的用法,实际上传 setConfig 更简洁。
常见错误
1. useCallback 依赖数组漏掉 state
tsx
// ❌ 错误:handler 里用了 config,但没写进依赖
const handler = useCallback((field) => {
doSomething(config, field); // config 是 stale(过期)的
}, []);
// ✅ 正确
const handler = useCallback((field) => {
doSomething(config, field);
}, [config]);2. 到处加 React.memo 反而更慢
每次渲染都需要做 props 浅比较,如果 props 每次都变(比如传了新对象),比较本身也有开销。
原则:先测量,再优化。
3. 误以为 React.memo 解决了所有重渲染
tsx
// ❌ 误区:用了 memo 就一定不重渲染
const Child = memo(function Child({ onClick }) { ... });
// 父组件每次渲染都传新的 onClick
<Child onClick={() => doSomething()} />
// ↑ 即使有 memo,onClick 每次都是新函数,子组件还是会重渲染React.memo + useCallback 要配合用。
动手改一改
- 打开浏览器控制台,在
ComponentSidebar组件里加console.log('ComponentSidebar render') - 修改一下配置表单,观察控制台输出
- 用
memo包裹后再试,验证它真的不重渲染了
验收清单
- [ ]
ComponentSidebar用了memo包裹 - [ ]
handleChange用了useCallback - [ ] 在控制台验证了
ComponentSidebar不再频繁重渲染 - [ ] 能解释
React.memo如何比较 props - [ ] 能解释为什么
useCallback要配合React.memo使用 - [ ] 能说出什么时候不需要
useCallback
今日记录
今天跑通:
- React.memo 避免无效重渲染
- useCallback 稳定回调引用
- 用 DevTools 验证优化效果
现在能解释:
- 浅比较是什么
- useCallback 的依赖数组
- 什么时候值得优化,什么时候不值得
明天先做:
- 代码整理和最终验收
- 整理组件树,确保每个组件职责清晰
留给明天的接口
功能全部完成,明天做最后的代码整理:
- 检查每个组件的职责是否清晰
- 提取重复的 inline style 到常量
- 最终验收项目
进行中