React 避坑指南:彻底搞定不必要的重新渲染

"我的页面就改了个弹窗开关,为什么整个列表都闪了一下?"

------ 如果你也曾这样怀疑人生,这篇内容就是为你写的。


一、为什么"重新渲染"这么贵?

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)

  1. 输入字符 → 根组件 setState → 整页重新渲染;
  2. 列表项组件未 memo,每条都执行 render;
  3. 过滤函数在 render 内执行,O(n) 复杂度 × 500;
  4. 回调函数每次新建,导致 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 个"反模式"

  1. 在 render 里写箭头函数
    <button onClick={() => handleClick(id)} />

    → 每次新引用,子组件 memo 失效。

  2. 在 render 里新建对象
    <Child style={{ color: 'red' }} />

    → 同上,用 useMemo 或提前声明常量。

  3. 把函数组件写在另一个组件内部

    见上文"新建组件类型"。

  4. 滥用 key 来"刷新"组件

    只有身份变更才改 key,局部数据更新请用状态。

  5. Context value 直接传对象字面量

    js 复制代码
    <AuthCtx value={{ user, setUser }}>  

    → 每次渲染都生成新对象,所有消费者重渲染。

    解决:

    js 复制代码
    const value = useMemo(() => ({ user, setUser }), [user]);

五、checklist:上线前必查的 8 个问题

  1. 所有纯展示组件都包 React.memo 了吗?
  2. 传给 memo 组件的函数都用 useCallback 缓存了吗?
  3. 对象/数组 props 都用 useMemo 缓存了吗?
  4. 状态是否放在"最近公共祖先"?
  5. 列表 key 用的是稳定 ID 而不是索引?
  6. 过滤/排序放在 render 里了吗?挪进 useMemo
  7. 有 props drilling 超过 3 层吗?考虑 Context 或状态库。
  8. 用 Profiler 录制一次主流程,确认没出现"黄色长条"。

六、写在最后

优化重新渲染,就像给 React 应用做"断舍离":
少一点不必要的更新,多一点丝滑的体验。

把这 10 个招式练成肌肉记忆,你也能在 Code Review 时自信地留下那句:

"这段我 memo 过了,放心合并。"

祝你的下一次 npm start 不再闪屏,愿所有列表都如丝般顺滑。

相关推荐
用户629605932470513 分钟前
从“浏览器根本区分不了”到真正实现:刷新不退出,关闭标签页自动退出,前端判断浏览器关闭和刷新
前端
qetfw14 分钟前
MWU:Vue 3 + FastAPI 的 MaaFramework 跨平台 WebUI 源码
前端·vue.js·python·fastapi·开源项目·效率工具
mfxcyh16 分钟前
Vue3+Ant Design Vue 实现手动异步文件上传(含大小校验、弹窗上传)
前端·javascript·vue.js
wordbaby17 分钟前
Web端热敏标签打印完整解决方案(QZ Tray + 译维A42)
前端
swipe21 分钟前
03|Axios 请求进了后端之后:Controller、Request、Response 是怎么接住它的?
前端·后端·全栈
meilindehuzi_a26 分钟前
Vue3进阶基础:指令修饰符、v-model表单绑定、动态样式、计算属性与侦听器实战
前端·javascript·vue.js
csdn2015_32 分钟前
vue 前端运行命令
前端·javascript·vue.js
swipe33 分钟前
02|从 `pnpm dev` 到 Spring Boot 启动:后端服务到底怎么跑起来?
前端·后端·全栈
swipe1 小时前
01|前端人第一次打开 Spring Boot 项目,应该先看哪里?
前端·后端·全栈
Cobyte1 小时前
根据浏览器解析 HTML 的规范实现 HTML 解析器
前端·javascript·vue.js