"我的页面就改了个弹窗开关,为什么整个列表都闪了一下?"
------ 如果你也曾这样怀疑人生,这篇内容就是为你写的。
一、为什么"重新渲染"这么贵?
React 的默认行为是:父组件一更新,所有子组件无条件跟着渲染 。
在小型 Demo 里这没什么,但在真实业务里:
- 列表 1000 项,每项 20 个 DOM,一次渲染就是 2 万次比对;
- 图表、表格、富文本编辑器初始化一次要几百毫秒;
- 低端手机掉帧、风扇狂转、用户开始骂娘。
性能优化第一性原理 :
"不要阻止 React 更新,而是让它根本没必要更新。"
二、10 个实战招式,招招致命
| 招式 | 一句话记忆 | 典型场景 | 代码片段 |
|---|---|---|---|
| ① React.memo | "子组件 props 没变就别来烦我" | 纯展示组件 | export default React.memo(Card) |
| ② useCallback | "函数引用给我稳住" | 传给子组件的回调 | const onClick = useCallback(() => {}, [id]) |
| ③ useMemo | "对象/数组也给我稳住" | 过滤、排序、映射 | const list = useMemo(() => filter(raw), [raw, key]) |
| ④ 状态下放 | "状态别放爷爷组件" | 弹窗、开关、表单 | 把 isOpen 放到 <Modal> 父级,而非 App 根 |
| ⑤ 拆分小组件 | "大组件=大锅饭" | 列表项、卡片 | 1000 行上帝组件拆成 20 行 Item 组件 |
| ⑥ 容器/展示 | "数据层与视图层离婚" | 任何页面 | 容器取数 → 展示 memo |
| ⑦ Context 拆值 | "Context value 别每次新对象" | 主题、用户 | 用 useMemo 包 value,或拆成两个 Context |
| ⑧ 禁用 render 新建组件 | "别在函数里写函数组件" | 动态渲染 | 组件定义放顶层 |
| ⑨ 状态管理库 | "props drilling 超过 3 层就上库" | 跨页面共享 | Zustand / Redux / Jotai |
| ⑩ Profiler 复盘 | "先量化,再优化" | 任何优化前后 | React DevTools → Profiler → 录制 → 对比 |
三、案例复盘:一个"搜索列表"从 450 ms 到 45 ms
1. 背景
- 商品列表 500 条,支持关键字搜索、分类筛选;
- 每次输入字符,整个页面闪一下,搜索框掉帧;
- 测试手机:Redmi Note 9。
2. 问题定位(Profiler)
- 输入字符 → 根组件 setState → 整页重新渲染;
- 列表项组件未 memo,每条都执行 render;
- 过滤函数在 render 内执行,O(n) 复杂度 × 500;
- 回调函数每次新建,导致 memo 失效。
3. 四步优化
Step 1 状态下放
把搜索关键字 keyword 从 App 根级放到 <SearchList> 组件,其他模块不再受牵连。
Step 2 缓存计算
js
const filtered = useMemo(
() => goods.filter(g => g.name.includes(keyword)),
[goods, keyword]
);
Step 3 缓存回调
js
const onAddCart = useCallback((id) => cart.add(id), [cart]);
Step 4 列表项 memo
js
const Item = React.memo(({ goods, onAddCart }) => (
<div>
<span>{goods.name}</span>
<button onClick={() => onAddCart(goods.id)}>加购</button>
</div>
));
4. 结果
- 输入字符时,仅 SearchList 与列表重新渲染,Header、Sidebar、Footer 纹丝不动;
- 单次渲染耗时从 450 ms → 45 ms;
- 掉帧率从 18% → 0%。
四、最容易踩的 5 个"反模式"
-
在 render 里写箭头函数
<button onClick={() => handleClick(id)} />→ 每次新引用,子组件 memo 失效。
-
在 render 里新建对象
<Child style={{ color: 'red' }} />→ 同上,用
useMemo或提前声明常量。 -
把函数组件写在另一个组件内部
见上文"新建组件类型"。
-
滥用 key 来"刷新"组件
只有身份变更才改 key,局部数据更新请用状态。
-
Context value 直接传对象字面量
js<AuthCtx value={{ user, setUser }}>→ 每次渲染都生成新对象,所有消费者重渲染。
解决:
jsconst value = useMemo(() => ({ user, setUser }), [user]);
五、checklist:上线前必查的 8 个问题
- 所有纯展示组件都包
React.memo了吗? - 传给 memo 组件的函数都用
useCallback缓存了吗? - 对象/数组 props 都用
useMemo缓存了吗? - 状态是否放在"最近公共祖先"?
- 列表 key 用的是稳定 ID 而不是索引?
- 过滤/排序放在 render 里了吗?挪进
useMemo! - 有 props drilling 超过 3 层吗?考虑 Context 或状态库。
- 用 Profiler 录制一次主流程,确认没出现"黄色长条"。
六、写在最后
优化重新渲染,就像给 React 应用做"断舍离":
少一点不必要的更新,多一点丝滑的体验。
把这 10 个招式练成肌肉记忆,你也能在 Code Review 时自信地留下那句:
"这段我 memo 过了,放心合并。"
祝你的下一次 npm start 不再闪屏,愿所有列表都如丝般顺滑。