如何减少 React 组件的无意义重复渲染?
一、先搞懂:组件为啥会无意义重渲染?
React 组件重渲染的核心触发条件是:
- 组件自身
state变化; - 父组件重渲染(即使传给子组件的 props 没变化);
- 组件使用的
context数据变化; - 用了
useState/useReducer但依赖没写对。
「无意义重渲染」就是:组件满足上述条件,但没有任何 UI 需要更新(比如父组件点了个按钮,子组件没用到父组件的任何数据,却跟着渲染)。
二、核心解决方案(按优先级排序,越靠前越易落地)
1. 用 React.memo 缓存纯组件(最常用)
React.memo 是高阶组件,会浅比较子组件的 props,只有 props 真变化时才重渲染。- 适用场景:子组件是「纯组件」(输出只由 props 决定,无内部 state / 副作用)。
- 错误示范:父组件重渲染,子组件无意义跟着渲染
jsx
// 父组件 function Parent() { const [count, setCount] = useState(0); // 每次渲染都会创建新的handleClick函数(引用变化) const handleClick = () => setCount(count + 1); return ( <div> <button onClick={handleClick}>点击{count}次</button> {/* 即使msg没变化,子组件也会跟着父组件渲染 */} <Child msg="固定文本" /> </div> ); } // 子组件:无memo,父渲染则子渲染 function Child({ msg }) { console.log("子组件无意义渲染了!"); return <div>{msg}</div>; } - 优化方案:用
memo包裹子组件jsx// 子组件:加memo,仅props变化时渲染 const Child = memo(({ msg }) => { console.log("子组件只在props变化时渲染!"); return <div>{msg}</div>; }); - 注意:
memo是「浅比较」,如果 props 是对象 / 数组(引用类型),即使内容没变化,引用变了也会触发渲染(后续用useCallback/useMemo解决)。
2. 用 useCallback 缓存函数型 props
父组件传给子组件的函数,每次渲染都会创建新引用,即使
memo 也会失效 —— 用 useCallback 缓存函数引用,只有依赖变化时才重新创建。- 错误示范:memo 失效(函数 props 引用变化)
jsx
function Parent() { const [count, setCount] = useState(0); // 每次渲染创建新函数,Child的memo失效 const handleChildClick = () => console.log("点击子组件"); return ( <div> <button onClick={() => setCount(count + 1)}>点击{count}次</button> <Child onClick={handleChildClick} /> </div> ); } const Child = memo(({ onClick }) => { console.log("memo失效,还是渲染了!"); return <button onClick={onClick}>子组件按钮</button>; }); - 优化方案:用
useCallback缓存函数jsxfunction Parent() { const [count, setCount] = useState(0); // 缓存函数,只有依赖(空数组)变化时才重新创建 const handleChildClick = useCallback(() => { console.log("点击子组件"); }, []); // 依赖为空,函数永远不重新创建 return ( <div> <button onClick={() => setCount(count + 1)}>点击{count}次</button> <Child onClick={handleChildClick} /> </div> ); } const Child = memo(({ onClick }) => { console.log("只有onClick变化时才渲染!"); return <button onClick={onClick}>子组件按钮</button>; });
3. 用 useMemo 缓存对象 / 数组型 props
和函数同理,对象 / 数组每次渲染会创建新引用,导致
memo 失效 —— 用 useMemo 缓存引用类型 props。- 错误示范:对象 props 引用变化导致 memo 失效
jsx
function Parent() { const [count, setCount] = useState(0); // 每次渲染创建新对象,Child的memo失效 const userInfo = { name: "张三", age: 20 }; return ( <div> <button onClick={() => setCount(count + 1)}>点击{count}次</button> <Child user={userInfo} /> </div> ); } const Child = memo(({ user }) => { console.log("对象引用变了,memo失效!"); return <div>{user.name}</div>; }); - 优化方案:用
useMemo缓存对象jsxfunction Parent() { const [count, setCount] = useState(0); // 缓存对象,依赖为空则引用不变 const userInfo = useMemo(() => ({ name: "张三", age: 20 }), []); return ( <div> <button onClick={() => setCount(count + 1)}>点击{count}次</button> <Child user={userInfo} /> </div> ); } const Child = memo(({ user }) => { console.log("只有user内容变了才渲染!"); return <div>{user.name}</div>; });
4. 拆分组件,缩小重渲染范围
把大组件拆成「稳定部分」和「变化部分」,让变化部分只影响小范围组件,避免整个大组件重渲染。
- 错误示范:大组件包含稳定 UI 和变化 UI,一点变化全渲染
jsx
// 大组件:头部(稳定)+ 计数器(变化)+ 列表(稳定) function BigComponent() { const [count, setCount] = useState(0); return ( <div> {/* 稳定UI:每次count变化也会跟着渲染 */} <Header title="固定标题" /> {/* 变化UI */} <button onClick={() => setCount(count + 1)}>{count}</button> {/* 稳定UI:每次count变化也会跟着渲染 */} <List data={[1,2,3]} /> </div> ); } - 优化方案:拆分出变化组件,稳定组件不受影响
jsx
// 拆分变化部分为独立组件 function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } // 大组件:只包含稳定UI,变化UI拆出去 function BigComponent() { return ( <div> <Header title="固定标题" /> <Counter /> {/* 只有这个组件会重渲染 */} <List data={[1,2,3]} /> </div> ); }
5. 优化 Context 使用(避免全局重渲染)
如果用
createContext 传全局数据,一旦 context 值变化,所有消费该 context 的组件都会重渲染 —— 解决方案:- 拆分 Context:把高频变化和低频变化的 Context 分开(比如「用户登录状态」和「主题设置」拆成两个 Context);
- 用
useContext只在必要组件中消费:不要在父组件消费 Context 再传给子组件,让子组件直接消费(减少父组件重渲染带动子组件)。
6. 用 useRef 替代不必要的 state
如果数据变化不需要触发 UI 更新(比如定时器 ID、DOM 元素、临时数据),用
useRef 代替 useState,避免无意义重渲染。- 错误示范:用 state 存不需要 UI 更新的数据
jsx
function Timer() { // 定时器ID变化不需要UI更新,用state会触发渲染 const [timerId, setTimerId] = useState(null); const start = () => { const id = setInterval(() => console.log("计时"), 1000); setTimerId(id); // 触发无意义重渲染 }; return <button onClick={start}>开始计时</button>; } - 优化方案:用 useRef 存非 UI 数据
jsx
function Timer() { const timerId = useRef(null); // 变化不触发渲染 const start = () => { timerId.current = setInterval(() => console.log("计时"), 1000); }; return <button onClick={start}>开始计时</button>; }
三、避坑要点(新手最容易踩的坑)
- 不要滥用 memo/useCallback/useMemo:
- 这些 API 本身有「浅比较成本」,如果组件渲染本身很快(<1ms),用了反而增加开销;
- 只在「重渲染次数多、渲染耗时高」的组件上用(比如大数据列表、复杂表单)。
- useCallback/useMemo 的依赖要写全:
- 漏写依赖会导致缓存的函数 / 数据不更新,出现 BUG(可以用 ESLint 规则
react-hooks/exhaustive-deps强制检查)。
- 漏写依赖会导致缓存的函数 / 数据不更新,出现 BUG(可以用 ESLint 规则
- memo 对「内部有 state/useContext 的组件」无效:
- memo 只能阻止「props 不变但父渲染」的情况,如果组件自身 state/context 变化,还是会渲染(这是正常的)。
四、实战验证:用 React Profiler 确认优化效果
优化后按以下步骤验证:
- 打开 React DevTools → Profiler,录制组件操作;
- 看目标组件的「渲染次数」是否减少(比如从 10 次→1 次);
- 看「渲染原因」是否只有「必要的 props/state 变化」(而非「Parent rendered」)。
总结
- 核心手段:memo(缓存组件)+ useCallback(缓存函数)+ useMemo(缓存引用类型)是解决无意义重渲染的「三板斧」;
- 优化逻辑:让组件只依赖「真正需要的变化数据」,缩小重渲染范围;
- 避坑关键:不滥用缓存 API,依赖写全,只优化耗时高的组件。

浙公网安备 33010602011771号