开篇:你是否遇到过这种"卡顿"?
想象一个场景:
你在开发一个电商网站 ,有一个商品列表组件 ,还有一个顶部的计数器按钮。
你点一下计数器 → 计数器变了。
结果呢?整个商品列表也重新渲染了一遍。
如果商品只有 10 个,你感觉不到。
但如果商品有 1000 个,而且每个都要做复杂的过滤、排序、渲染,你就会明显感觉到:点一下按钮,卡一下。
这就是 React 默认的"无脑重渲染"机制 导致的性能浪费。
怎么办?
用 memo、useMemo 和 useCallback!
它们就是 React 给我们准备的**"缓存神器"**,今天一次性讲清楚,看完再也不迷糊。
一、三个核心 API 一句话记住(新手必背)
先上干货,不用死记硬背,理解就好:
-
memo :缓存组件,props 不变就不重新渲染
-
useMemo :缓存计算结果,依赖不变就不重新计算
-
useCallback :缓存函数,依赖不变就不生成新函数
划重点:它们都是性能优化工具,不是必写代码!只有组件卡顿、出现不必要的重复渲染/计算时,再用才有用。普通简单组件用了反而会增加性能开销,得不偿失。
二、memo:给组件"贴保鲜膜",防止无故重渲染
2.1 它是干嘛的?
memo(全称 memory)是一个高阶组件,简单说就是给函数组件套一层"保护罩",让它变得"聪明":只有接收的 props 真正发生变化时,才重新渲染;props 没变,就直接复用上次的渲染结果。
2.2 没有 memo 会怎样?
React 有个默认机制:父组件重新渲染,所有子组件都会无条件跟着重新渲染,哪怕子组件的 props 一点没变。
看这个简单示例:
javascript
// 普通子组件(无 memo)
function NormalChild() {
console.log('🔴 普通子组件渲染了!');
return <div>普通子组件</div>;
}
// 父组件
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<NormalChild />
</div>
);
}
你点一下"+1"按钮,父组件更新,哪怕 NormalChild 没有接收任何 props,也会跟着渲染,控制台会一直打印"🔴 普通子组件渲染了!"------ 这就是纯粹的性能浪费。
2.3 加上 memo 后,效果立竿见影
javascript
import { memo } from 'react';
// 被 memo 包裹的子组件
const MemoChild = memo(() => {
console.log('🟢 Memo子组件渲染了!');
return <div>Memo子组件</div>;
});
// 父组件不变
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<MemoChild />
</div>
);
}
现在再点"+1",父组件更新,但 MemoChild 完全无动于衷,控制台不会再打印新的日志 ------ 因为它的 props 没变,memo 帮它"跳过"了重渲染。
2.4 什么时候用 memo?
-
子组件是纯展示型(只负责渲染,不做复杂逻辑),且 props 很少变化;
-
子组件渲染开销很大(比如长列表、复杂图表、嵌套层级多的表单);
-
父组件经常重新渲染,但子组件的 props 基本不变。
2.5 一个致命坑点!(新手必踩)
memo 对 props 的比较是浅比较 (Shallow Comparison)。如果你的 props 是对象、数组、函数这类引用类型,哪怕内容完全一样,但引用地址变了,memo 也会认为"props 变了",从而触发子组件重新渲染。
尤其是函数类型的 props,这是最容易踩坑的场景 ------ 哪怕你写的函数逻辑完全没变,父组件每次渲染都会生成一个新的函数,导致 memo 直接失效。
比如这段错误示范:
javascript
// 子组件(memo 包裹)
const MemoChild = memo(({ handleClick }) => {
console.log('🟢 Memo子组件渲染了!');
return <button onClick={handleClick}>点击</button>;
});
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 父组件每次渲染,都会创建一个新的 handleClick 函数!
const handleClick = () => {
console.log('点击了');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<MemoChild handleClick={handleClick} />
</div>
);
}
你点"+1",父组件更新 → 生成新的 handleClick 函数 → memo 浅比较发现函数引用变了 → 子组件被迫重新渲染。明明加了 memo,却没起到作用 ------ 这时候,就需要 useCallback 登场了!
三、useCallback:专门缓存函数,解决 memo 失效问题
3.1 它是干嘛的?
useCallback 是一个 React Hook,核心作用只有一个:缓存函数本身。让函数在"依赖项不变"的情况下,永远保持同一个引用地址,不会因为父组件渲染而重新生成新函数。
它就像给函数"存了个档",只要依赖没变,每次取出来的都是同一个"版本",这样 memo 做浅比较时,就会认为 props 没变,从而阻止子组件重渲染。
3.2 基本用法
javascript
import { useCallback } from 'react';
// 语法:useCallback(函数体, 依赖数组)
const 缓存后的函数 = useCallback(() => {
// 函数逻辑
}, [依赖项1, 依赖项2]);
和 useMemo 类似,依赖数组是关键:
-
依赖数组为空 []:函数只会生成一次,永远不变;
-
依赖数组有值:只有依赖项发生变化时,才会生成新函数。
3.3 搭配 memo 使用,彻底解决函数 props 问题
把上面的错误示范改成正确写法,只需要加一行 useCallback:
javascript
import { memo, useCallback, useState } from 'react';
// 子组件(memo 包裹,不变)
const MemoChild = memo(({ handleClick }) => {
console.log('🟢 Memo子组件渲染了!');
return <button onClick={handleClick}>点击</button>;
});
// 父组件(用 useCallback 缓存函数)
function Parent() {
const [count, setCount] = useState(0);
// 用 useCallback 缓存 handleClick,依赖为空,永远是同一个函数
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 依赖为空,函数引用不变
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<MemoChild handleClick={handleClick} />
</div>
);
}
现在再点"+1",父组件更新,但 handleClick 的引用没变 → memo 浅比较通过 → 子组件不再重新渲染!完美解决问题。
3.4 什么时候用 useCallback?
-
子组件用 memo 包裹,且需要接收函数类型的 props(最核心场景);
-
函数需要作为依赖项传入其他 Hook(比如 useEffect),避免每次渲染都触发 useEffect;
-
父组件频繁更新,且函数逻辑不变,需要避免函数重复生成。
3.5 常见误区
❌ 不要给所有函数都加 useCallback:简单函数(比如 console.log)本身生成开销很小,加 useCallback 反而会增加缓存的开销,得不偿失;
❌ 依赖数组不要漏写:如果函数里用到了父组件的 state、props,一定要把它们加入依赖数组,否则会出现"闭包陷阱"(函数里拿到的是旧的 state/props);
✅ 正确示例(函数用到 state,依赖数组加 count):
javascript
const handleClick = useCallback(() => {
console.log('count:', count); // 用到了 count
}, [count]); // 必须把 count 加入依赖
四、useMemo:给计算结果"存草稿",避免重复计算
4.1 它是干嘛的?
useMemo 也是一个 React Hook,核心作用是缓存复杂计算的结果。如果组件里有耗时的计算(比如大数据过滤、排序、多轮循环),每次渲染都重新计算会非常浪费性能,用 useMemo 可以让计算结果"存档",依赖不变就不用重新算。
形象比喻:就像你做数学题,算完一次把结果写在草稿纸上,下次再用到时,直接看草稿纸,不用再重新算一遍。
4.2 没有 useMemo 会怎样?
看这段代码:
javascript
function Parent() {
const [list, setList] = useState(/* 1000条数据 */);
// 复杂计算:过滤并排序1000条数据
const filteredList = list.filter(item => item.status === 1).sort((a, b) => a.time - b.time);
return <MemoChild list={filteredList} />;
}
父组件每次渲染(哪怕只是修改了一个无关的 state),filteredList 都会重新计算一次 ------ 1000条数据的过滤+排序,次数多了就会卡顿。
4.3 加上 useMemo 后
javascript
import { useMemo, useState } from 'react';
function Parent() {
const [list, setList] = useState(/* 1000条数据 */);
// 用 useMemo 缓存计算结果,只有 list 变了才重新算
const filteredList = useMemo(() => {
return list.filter(item => item.status === 1).sort((a, b) => a.time - b.time);
}, [list]); // 依赖 list
return <MemoChild list={filteredList} />;
}
现在,只有 list 发生变化时,才会重新执行过滤和排序;list 不变,直接复用上次的计算结果 ------ 性能瞬间提升。
4.4 什么时候用 useMemo?
-
组件内部有明显耗时的计算(过滤、排序、大数据处理、复杂格式化);
-
计算结果要传给 memo 子组件做 props(搭配 memo 使用,双重优化);
-
计算逻辑重复执行,且结果在依赖不变时不会变化。
4.5 与 useCallback 的区别
很多新手会混淆 useMemo 和 useCallback,一句话分清:
-
useMemo:缓存函数的返回值(计算结果);
-
useCallback:缓存函数本身(函数引用)。
简单说:需要缓存"值",用 useMemo;需要缓存"函数",用 useCallback。
五、黄金搭档:memo + useCallback + useMemo(工作中最常用)
实际开发中,这三个 API 很少单独使用,搭配起来才能实现最大化的性能优化,尤其是处理复杂列表、大数据场景时。
核心组合逻辑:
-
子组件用 memo 包裹,防止不必要的重渲染;
-
给子组件传的函数 props,用 useCallback 缓存,避免 memo 失效;
-
给子组件传的计算型 props,用 useMemo 缓存,避免重复计算。
完整可运行 Demo(包含所有优化)
复制这段代码,在 Create React App 中直接运行,打开控制台点击按钮,就能直观看到优化效果:
javascript
import { useState, memo, useMemo, useCallback } from 'react';
// 1. memo 缓存子组件(接收函数和计算结果 props)
const ListChild = memo(({ items, handleItemClick }) => {
console.log('🟢 列表子组件渲染');
return (
<ul style={{ marginTop: 20 }}>
{items.map(item => (
<li key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 2. 父组件
export default function App() {
const [list, setList] = useState([
{ id: 1, name: "苹果", status: 1 },
{ id: 2, name: "香蕉", status: 0 },
{ id: 3, name: "橙子", status: 1 },
{ id: 4, name: "葡萄", status: 1 }
]);
const [count, setCount] = useState(0);
// 3. useMemo 缓存复杂计算结果(过滤状态为1的商品)
const filteredItems = useMemo(() => {
console.log('🧮 useMemo 计算执行');
return list.filter(item => item.status === 1).sort((a, b) => a.id - b.id);
}, [list]); // 依赖 list,只有 list 变才重新计算
// 4. useCallback 缓存函数(传给子组件的点击事件)
const handleItemClick = useCallback((id) => {
console.log('点击了商品:', id);
}, []); // 依赖为空,函数引用不变
console.log('------------------ 父组件渲染 ------------------');
return (
<div style={{ padding: 20 }}>
<h1>count: {count}</h1>
<button onClick={() => setCount(count + 1)}>+1(无关操作)</button>
<h3>过滤后的商品列表(只显示状态为1的)</h3>
<ListChild items={filteredItems} handleItemClick={handleItemClick} />
</div>
);
}
Demo 效果说明(打开控制台查看)
-
点击"+1"按钮,父组件渲染(控制台打印"父组件渲染");
-
ListChild 子组件不会渲染(控制台不打印"列表子组件渲染")------ 因为 memo + useCallback + useMemo 一起生效了;
-
useMemo 里的计算不会执行(控制台不打印"useMemo 计算执行")------ 因为 list 没变;
-
只有当 list 变化时(比如添加、删除商品),filteredItems 才会重新计算,ListChild 才会重新渲染。
六、核心补充:useCallback 依赖相关(新手必看)
针对 useCallback 的依赖数组,直接讲核心(可直接用于开发参考):
1. 依赖为空 [] 的含义
当 useCallback 的依赖数组写 [] 时,意味着这个函数不依赖父组件任何 state、props 等变量,函数逻辑固定不变。
示例:
javascript
const handleItemClick = useCallback((id) => { console.log('点击了商品:', id); }, []);
这个函数只打印商品id,不用到父组件任何变量,因此仅在父组件第一次渲染时生成一次,后续父组件无论怎么重新渲染,函数引用都不会变,能配合 memo 阻止子组件无故重渲染。
2. 函数依赖变量时的写法(核心原则:用到什么,加什么)
如果函数逻辑中用到了父组件的 state、props,必须把这些变量全部加入依赖数组,否则会出现"闭包陷阱"(函数中拿到的是旧的变量值)。
场景1:依赖父组件 state(如 count)
javascript
const [count, setCount] = useState(0);
// 用到 count,必须加入依赖
const handleClick = useCallback(() => {
console.log('当前count:', count);
setCount(count + 1);
}, [count]);
场景2:依赖父组件 props(如 title)
javascript
function Parent({ title }) {
// 用到 props.title,加入依赖
const handleShowTitle = useCallback(() => {
alert(`标题:${title}`);
}, [title]);
return <MemoChild onShowTitle={handleShowTitle} />;
}
场景3:依赖多个变量(state + props)
javascript
function Parent({ title }) {
const [count, setCount] = useState(0);
// 用到 title 和 count,全部加入依赖
const handleCombine = useCallback(() => {
console.log(`标题:${title},count:${count}`);
}, [title, count]);
return <MemoChild onCombine={handleCombine} />;
}
关键总结
useMemo 依赖数组规则,和 useCallback 完全通用:
- 无依赖变量 → 写
[],计算仅执行一次,永久复用; - 有依赖变量 → 用到的变量全部加入,变量变则重新计算。
七、避坑指南(新手必看,少走弯路)
1. 不要滥用优化 API
memo、useMemo、useCallback 都有轻微的性能开销(创建缓存、比较依赖)。对于简单组件、简单函数、简单计算,完全不需要用 ------ 比如一个只显示文字的子组件,加 memo 反而会变慢。
2. 先找瓶颈,再优化
不要上来就给所有组件、函数、计算都加缓存。先用 React 官方的 React DevTools 的 Profiler 功能,分析出到底是哪个组件渲染慢、哪个计算重复执行,再针对性优化。
3. 依赖数组一定要写完整
useMemo 和 useCallback 的依赖数组,必须包含函数体里用到的所有 state、props ------ 漏写会导致闭包陷阱,比如拿到旧的 state 值,出现 bug。
4. 引用类型 props 必须缓存
如果给 memo 子组件传的 props 是对象、数组、函数,一定要用 useMemo(对象/数组)、useCallback(函数)缓存,否则 memo 会失效。
5. memo 不是"万能的"
memo 只能优化"props 不变时的重渲染",如果子组件本身依赖了全局状态(比如 Redux)、context,或者自己有 state 变化,memo 是无法阻止它重渲染的 ------ 它只管 props。
八、最终总结(一张表看懂所有区别)
| API | 核心作用 | 缓存对象 | 解决的问题 | 常用搭配 |
|---|---|---|---|---|
| memo | 缓存组件 | 组件实例 | 子组件不必要的重渲染 | useCallback、useMemo |
| useMemo | 缓存计算结果 | 函数返回值 | 复杂计算重复执行 | memo(给子组件传计算值) |
| useCallback | 缓存函数 | 函数本身 | 函数 props 导致 memo 失效 | memo(给子组件传函数) |
最后记住一句话:性能优化是为了解决实际问题,而不是为了优化而优化。先写出能正常运行的代码,再根据实际卡顿情况,用这三个神器针对性优化,你的 React 项目就能变得更流畅、更高效。