
做过 React 项目的同学大概率都遇到过这样的场景功能不复杂数据量也不算大但只要某处 state 一更新整个页面就像被按了慢放键输入框敲字都卡。刚开始我以为是 React 虚拟 DOM diff 太慢后来把 shouldComponentUpdate 翻来覆去地加代码丑得自己都不忍直视性能却没什么起色。直到我花了一整个下午去逐层排查组件的渲染链路才意识到问题根本不是 React 本身而是组件设计和通信方式把渲染范围撑得太大了。这篇文章我想把这两年做 React 项目踩过的坑和总结出来的方法论一次性聊透。主题就是标题里那三件事性能优化、组件设计模式、组件通信。它们表面上是三个话题实际是一个问题的三个侧面——组件之间怎么通信决定了状态在哪个范围变化状态在哪个范围变化决定了哪些组件要重新渲染哪些组件要重新渲染决定了你的页面快不快。想通这条链路性能优化就不再是堆 memo 和塞各种缓存而是从设计源头把开销控制住。适合的读者正在做中型以上 React 项目的开发者对 Hooks、Context 有基础了解但觉得性能优化无从下手的朋友以及准备面试想把这几个知识点串成体系的人。全文我会按问题根源 → 设计模式 → 通信方案 → 渲染层实战 → 真实排查案例的顺序来讲尽量多给可以直接实操的细节。1. 性能问题的根源不是 React 慢而是你让它白干了太多活1.1 一次 setState 之后React 究竟在忙什么要谈性能优化先得把 React 的渲染机制搞清楚。这里说的渲染不是浏览器重新绘制页面而是 React 内部的一次协调过程当某个组件的 state 或 props 发生变化React 就会以这个组件为起点向下递归地执行 render 函数生成一棵新的虚拟 DOM 树然后和上一次的树做 diff找出差异最后才把差异提交给真实 DOM。这里有个关键点很多人没意识到React 默认是无脑的。父组件重新 render 了子组件就算 props 一点都没变也会跟着重新执行 render。这个跟着执行本身没有错React 要靠这个来保证数据一致性但如果你的组件树很大、子组件很多、每个 render 里还有复杂的计算和嵌套对象创建那一次 state 更新可能触发几百个组件的 render 函数执行。哪怕每个组件执行只要零点儿几毫秒加起来一次交互就得多花几十甚至上百毫秒用户感知到的就是卡顿。打个比方React 的默认行为就像一个快递公司任何一个小包裹到了不管是不是你的都要把所有快递员叫醒检查一遍货架。大部分时候这些检查都是徒劳的但公司为了不出错宁可每次都全员出动。1.2 三个最容易忽视的性能杀手我排查过不少线上性能问题总结下来绝大多数卡顿都离不开下面三个原因第一状态放得太高。很多开发者习惯把所有数据都放在顶层组件里管理然后通过 props 一层层往下传。这样做的后果是任何一个底层子组件想更新某个局部数据都得把状态提升到顶层然后再让整棵组件树重新渲染一遍。这种设计的本质问题是把通信成本和渲染范围同时拉满了。第二Context 滥用。Context 是官方提供的跨层级通信方案但它的价值语义在 React 内部其实很简单——只要 context value 变了所有订阅了这个 context 的组件都会强制重新渲染不管它们是否真的用到了变化的那部分数据。很多人把整个 store 对象塞进 context value导致任何一个小字段的更新都引发全局刷新。第三render 函数里产生新引用。这是最常见也最隐蔽的问题。比如在 render 里直接写Child onClick{() handleClick(id)}或者写const style { color: red }又或者对一个数组做.filter()之后传给子组件。这些操作每次 render 都会生成全新的对象引用如果子组件恰好被 React.memo 包裹了恭喜你memo 全部失效。这三个问题后面都会展开。先记住一个核心理念性能优化不是让 React 跑得更快而是让 React 尽量少跑。2. 组件设计模式从源头控制渲染范围2.1 容器组件与展示组件拆分把变和不变隔离容器组件和展示组件的拆分是我早期做 React 最受益的一个模式到现在依然是很多团队的基础规范。核心思想很简单把组件的数据逻辑和UI 呈现分开容器组件负责状态管理和数据获取展示组件只负责根据 props 渲染界面。为什么这个模式对性能有好处因为拆分之后你可以更精细地控制谁该重新渲染。数据更新通常只发生在容器组件那一层展示组件如果 props 没变化就能被 React.memo 完好地挡在渲染之外。如果不拆分把状态和 UI 混在一起状态一更新整个组件连带它所有的子组件全部重新 render完全没有隔离的余地。举个例子一个商品列表页有筛选条件、排序方式、商品卡片。如果所有状态都堆在列表页组件里用户点一下排序整个列表页从头部导航到页脚全部重新渲染。而如果用容器组件包住筛选逻辑用展示组件分别渲染列表头和商品卡片那么排序状态变化时只有列表容器和它直接依赖的展示组件会更新其他区域毫发无损。当然这个模式也有过度应用的时候。组件拆得越细props 就越多通信代码就越冗长。我的经验是拆分不是为了代码漂亮而是为了控制渲染范围和复用边界。如果一个展示组件内部只有几十行、没有重复使用的地方、父组件的更新频率也不高那拆不拆无所谓别为了模式而模式。2.2 从 HOC 到 Hooks设计模式的演进与取舍React 社区这些年围绕着怎么复用逻辑演化出了好几种模式Mixin、高阶组件HOC、Render Props、自定义 Hooks。每次演进都是对上一次缺点的修正理解这个过程能帮你避开很多历史坑。HOC 的本质是包装组件。函数接收一个组件返回一个增强后的新组件常见应用是权限校验、埋点上报、拉取数据然后注入 props。它的性能隐患在于每次 render 时如果动态创建 HOC会导致被包装组件重新挂载。比如你在 render 里写withAuth(MyComponent)每次 render 都会创建一个新的组件类型React 会认为这是完全不同的组件直接卸载旧的再挂载新的状态丢失、性能暴跌。正确做法是在模块顶层创建 HOC保证组件类型的引用稳定。Render Props 通过一个函数 prop 把状态传给子组件解决了 HOC 的部分问题但写法嵌套一多就容易变成回调地狱而且同样存在 render 函数里产生新函数引用的问题。Hooks 为什么是终极方案因为它把逻辑复用提升到了函数层面而不是组件层面。你不再需要包装组件也就不会引入多余的组件层级渲染树的深度变浅了React 协调的开销天然就小。自定义 Hook 还可以非常精准地控制副作用和缓存的粒度。所以新项目我几乎全部推荐 HooksHOC 和 Render Props 了解原理、能维护旧代码就够了。2.3 组合优于继承组件拆分粒度的把握设计组件树的时候很多人容易犯一个错误为了复用某些 UI 片段把组件拆得非常碎然后通过多层 props 一层层传数据。结果渲染的时候从根组件到叶子组件可能要经过五六层 props 透传每一层都要重新 render每一层都要传递大量 props——这就是典型的过度拆分。React 官方的建议是组合优于继承。所谓组合指的不是嵌套传 props而是用 children 或 slot 机制把子组件直接作为整体传入。这样做的好处非常明显父组件更新的 props 不会穿透到 children 内部去。举个典型的例子一个 Dashboard 布局组件左边是菜单右边是内容区。如果用 props 把菜单数据和内容数据都传进去任何一边更新都会带动整个布局重新渲染。但如果你用 children 把菜单和内容作为整体传进来那么只要布局组件自身状态不变children 引用不变React 就能跳过整个子树的重渲染。这里面的关键在于 children 本身也是一个对象引用。父组件 render 时如果 children 是上一次 createElement 的结果引用没变React 就会认为这棵子树不需要重新协调。所以我在实际项目里经常这样写状态变化只发生在布局这一层但 children 是从外部传入的那么布局层的重新渲染不会波及内容区。这个模式对页面框架类组件的优化效果极其显著。3. 组件通信的选型与实现不同距离用不同方案先说一句题外话。很多人会把 React 组件通信和 Electron 主进程与渲染进程的 IPC 通信混为一谈其实它们层级不同——前者是 React 应用内部的状态数据流后者是 JavaScript 运行环境之间的进程通信。但两者有个共同点通信方式选不好各自层的性能都会崩。Electron 里主进程和渲染进程之间用 IPC 还是直接共享状态决定了渲染进程会不会被主进程的每一条消息拖住React 里组件之间用 props、Context 还是状态库决定了状态更新会波及多大范围的组件。所以在做技术选型时先问一句这个数据到底需要影响多少人。3.1 父子通信props 和回调函数的正确姿势最基础也最容易被忽视的就是父子通信。父传子用 props子传父用回调函数这谁都知道但怎么传直接影响性能。先说 props。要保证子组件能享受 React.memo 的跳过渲染优化父组件传给子组件的 props 引用必须稳定。字符串、数字、布尔值这种原始类型天然稳定但对象、数组、函数就不一样了。在 render 函数里直接创建对象字面量、数组字面量、箭头函数每次 render 都是新的引用React.memo 对比新旧 props 时会发现引用不等于是白白触发子组件重渲染。正确的姿势是把函数用useCallback包起来把复杂对象用useMemo缓存或者干脆把函数定义提到组件外部不依赖 props 和 state 的纯函数才适合这么做。另外一个思路是如果子组件只需要原始值就别把整个对象传下去拆成单个原始值传。比如只需要user.name和user.isVip那就直接传这两个字符串和布尔值别传整个user对象这样即使父组件的user对象变化了只要这两个字段没变子组件就能被 memo 挡住。再说回调。子组件通过 props 里的回调通知父组件这种模式在通信距离只有一层时是最清晰的性能开销也最小因为数据流的走向一目了然状态在哪个组件、更新会触发哪些渲染都能顺着代码追踪。需要注意的坑是给子组件传多个回调时不要贪图方便传一个 dispatch 或 update 函数让子组件自己拼参数。那样看似灵活实际上把状态更新的逻辑分散到子组件里了将来排查谁改了这个状态会非常痛苦。3.2 跨层级通信Context 的性能陷阱与优化当数据要跨三层以上传递时逐层 props 穿透就成了灾难——中间层的组件既不使用这些数据却要为了传递而被迫重新渲染。Context 就是为解决这个场景而生的但它的性能问题也恰恰出在这儿。Context 的工作机制是Provider 的 value 引用一旦变化所有消费这个 context 的组件都会强制重新渲染。注意这个所有它是无差别的不会因为你用了 React.memo 就帮你挡掉——memo 在 context 面前是失效的因为消费组件不是因为 props 变化而重渲染而是因为 context 订阅被触发了。所以使用 Context 的第一个铁律value 必须用 useMemo 缓存并且粒度拆分清楚。别把整个 store 对象作为 value 传下去应该拆成多个 context或者让 value 只包含真正会变化、且消费方确实需要的那部分数据。举个例子主题色 context 和用户信息 context 就应该分开主题色变化时不需要让用户信息相关的组件跟着重新渲染。第二个铁律Provider 的位置尽量靠近消费方。Context 的层级越高受影响的组件树就越大。如果一个数据只被某个页面的某几个组件使用那 Provider 就放在那个页面的上层而不是挂在全局的根组件上。很多团队默认把 App 的根组件包一层全量 Provider结果任何一个全局状态更新整个应用全部刷新这是我在真实项目里见过最严重的性能事故之一。第三个进阶技巧如果确实需要把一部分数据放全局但又不希望全局组件全部重渲染可以考虑用一个状态选择器的机制——把 context 拆成两个一个存数据本身一个存 setter 函数。setter 的引用保持稳定用 useCallback这样消费组件订阅的是 setter 的 context数据更新时它们不会重渲染。这个模式在 Zustand 等状态库里有现成的 selector API 帮你做但如果用原生 Context就得手动控制这个粒度。提示原生 Context 没有内置 selector 能力每次读取都是按引用消费。如果你对渲染性能要求很高且状态较复杂优先考虑 Zustand 这类轻量状态库它们提供了非常精确的订阅机制能在组件粒度上避免无效渲染。3.3 全局通信事件总线与状态管理库的差异化选择有些场景是真正需要全局通信的用户登录状态、购物车数据、全局的 toast 通知、多模块之间互相联动。这类信息的传播路径往往很广用 Context 或 props 都不太合适。这时候通常有两个方向事件总线和状态管理库。事件总线比如 mitt、EventEmitter的思路是发布订阅任意两个模块可以通过事件名通信。它的优点是使用极其灵活、通信和 UI 完全解耦缺点是状态不透明——事件发出去了谁听了、谁改了、数据从哪来到哪去都要靠人肉维护大型项目里很容易失控。而且事件总线的数据更新不会自动和 React 生命周期绑定需要手动触发 setState稍不留神就会引入额外的渲染或漏更新。状态管理库Redux、Zustand、Jotai、Recoil本质上是把全局状态和组件订阅做了工程化的管理。我最推荐在当前新项目里用 Zustand原因有几个Redux 的样板代码太多action、reducer、selector 层层包裹对中小组件来说太重Zustand 的 store 可以在组件外部直接创建和更新组件通过 hook 按需订阅字段内部实现了精确到字段的订阅比较避免了整个 store 更新引发全应用刷新的问题样板代码极少天然支持依赖外部 store 的状态——这一点在处理 WebSocket 推送的场景里特别香。选用状态库的时候注意两个性能关键点一是订阅粒度。Zustand 默认按 selector 返回值做浅比较如果你在 selector 里返回一个每次新建的对象比如state.user.profile它会因为引用变化而陷入无限渲染循环或频繁重渲染正确的姿势是选择器返回原始值或者借助useShallow做浅层比较。二是避免把 store 塞进 Context。Zustand 的 store 是模块级单例组件直接 import 后 useStore根本不需要 Provider这又省掉了一层 context 消费者重渲染的开销。3.4 服务端通信SSE 和 WebSocket 场景下的组件更新策略现在前后端分离的应用里实时通信越来越常见SSE 和 WebSocket 已经是标配。它们对 React 组件通信的影响很少有人专门聊但实际踩坑的人非常多。先说 WebSocket。很多人习惯把 WebSocket 实例放在组件的 useEffect 里创建然后在 onmessage 回调里直接 setState。这种写法最大的问题是连接的生命周期和组件的挂载卸载耦合组件一卸载连接就断重新挂载又重连而且每个创建连接的组件都会拿到自己的回调闭包状态更新容易互相覆盖。我在做实时数据大屏的时候就吃过这个亏。稳妥的方案是把 WebSocket 的管理抽象成一个独立的服务模块用事件或状态库来做数据分发。连接只在应用层面建立一次比如做了一层connect()初始化收到消息后更新 Zustand store然后各个组件通过 selector 订阅自己关心的字段。这样通信的职责是统一的组件只关心数据来了之后我该渲染什么不关心数据是怎么来的。同时由于 store 的订阅是字段级的一条消息过来只有关心这个字段的组件会更新。再说明一下 SSE。它是基于 HTTP 的单向流只推送不接收所以比 WebSocket 简单得多常用于文件上传进度、服务端日志流、通知推送这类场景。React 侧的接法也类似用 useEffect 创建 EventSource收到 message 后更新局部 state 或者写入 store组件卸载时记得 close否则内存泄漏和重复监听会让性能逐渐恶化——这个坑我在线上遇到过打开页面又关掉几次EventSource 攒了一堆后台请求越积越多最后浏览器连接数被占满新请求全部卡住。如果你的场景里有轮询文件变化这类需求建议把轮询和推送做个区分轮询适用于服务端无法主动推送的场景但轮询间隔设置得太短会白白消耗带宽和 CPU如果服务端支持 SSE 或 WebSocket优先用推送把轮询作为降级方案。前端实现时把数据到达后的状态更新收敛到一个地方比如统一的 store action避免多个组件各自监听各自 setState 造成的重复渲染。4. 渲染层优化memo、缓存、懒加载的实战细节4.1 React.memo 不是万能药用对才有意义React.memo 是函数组件的浅比较缓存只有 props 变化时才会重新渲染。听起来很美好但实际项目中 memo 失效的场景占大多数原因就是我在前面反复提到的引用不稳定。典型场景如下// 父组件 function Parent() { const [count, setCount] useState(0); return ( Child data{{ count }} / ); }父组件每次 render 都创建一个新的对象{ count }即使 count 没变子组件也会重渲染因为 props 的引用每次都不同。这个模式下Child 加不加 memo 都一样。正确的做法之一是把对象创建移到 useMemoconst data useMemo(() ({ count }), [count]);之二是干脆拆开传原始值Child count{count} /所以我在团队评审代码时有个习惯看到别人的 memo 不生效第一反应不是怀疑 memo 有没有用而是先去看上游组件传给它的 props 是不是在 render 时新建了引用。memo 的意义是在props 引用稳定的前提下减少无效渲染它本身不能修复引用不稳定的问题。另外要注意memo 也不是越多越好。对于本身渲染成本极低、又经常因为父组件变化而需要同步更新的组件包一层 memo 反而增加对比开销。经验法则一个组件内部有复杂的计算、或者子树庞大、或者列表项很多才值得用 memo。简单的图标、按钮、文本节点别太执着。4.2 useMemo 和 useCallback缓存的是引用不是计算useMemo 和 useCallback 经常被误用。它们本质是依赖不变则引用不变的缓存工具。useCallback 就是 useMemo 的语法糖useCallback(fn, deps)等价于useMemo(() fn, deps)。最常见的误用是为了缓存计算结果而包裹所有计算结果发现依赖数组里塞了一大堆东西每次 render 依赖都变化缓存完全失效还额外执行了一次依赖比较。这个问题的根源是没搞明白 useMemo 的价值场景。useMemo 真正适合的场景是昂贵的计算比如遍历几千条日志、处理大量坐标点、复杂的字符串拼接。如果你的计算只是生成一个短字符串或者一个小数组useMemo 的意义不大因为 JS 引擎执行这些操作非常快比 useMemo 自身的开销还小。更常见的价值其实是引用稳定。真正需要 useMemo 的场景是你有一个数组或对象要传给子组件而这个派生逻辑又依赖一些可能变化的状态这时用 useMemo 保证依赖没变时引用不变从而让子组件的 memo 可以生效。这才是 useMemo 的主要用途——不是省计算而是省渲染。useCallback 也一样。传给子组件的回调函数应该尽量稳定尤其是配合 memo 的子组件。但我见过不少人把 useCallback 包在根本没有子组件消费的函数上纯粹为了形式。记住没有消费方缓存就没有意义只会白白增加闭包和依赖管理的复杂度。依赖数组的填写是个容易出错的细节。漏掉依赖会导致闭包捕获旧的 state行为错误填太多又会导致缓存失效。我的建议是写依赖的时候想想这个函数或数据真正依赖外部变化的变量有几个state 和 props 中哪些会影响结果只填这些。同时灵活运用函数式更新setCount(c c 1)这种写法不依赖外部 count是保持回调稳定的利器。4.3 大列表渲染虚拟滚动、Key 策略和渲染时机前端性能优化的重灾区永远是大列表。几千上万行的表格或日志如果每行都是一个组件一次渲染基本就卡死了。这里有三层手段可以叠加使用。第一层是虚拟滚动。react-window 和 react-virtualized 这两个库直接用视口高度计算可视区能容纳多少行只渲染可视区附近的组件滚动时动态替换。注意 react-virtualized 几乎已经不再维护新项目直接用 react-window 就好。它有两种形态FixedSizeList 适合行高固定的场景VariableSizeList 适合行高不定的场景。使用的时候有细节列表数据更新时如果数据总量变化要显式给列表设置 key 或调整 overscan 数量不然可能出现滚动位置错乱。第二层是Key 策略。列表项的 key 必须稳定且唯一我建议用业务 id而不是数组索引。用索引当 key 的问题在于当你删除或插入列表项时React 复用组件时会遇到序号错位导致组件内部状态比如输入框的内容、展开的折叠状态串到别的行上而且常见的删除中间一项后整个列表卡顿很大程度也是因为 key 用了索引React 不得不重新执行一串组件的挂载。第三层是渲染时机。列表本身如果不需要实时更新就不该放在高频状态变化的作用域下。比如一个数据监控页面图表区每秒刷新数据但日志列表是 5 秒刷新一次那就让日志列表用 useMemo 包住它的数据源或者干脆把列表做成独立容器组件避免被图表区的 state 更新波及。还有一个常见优化是把计算挪到渲染之外。比如列表项里做了字符串拼接、日期格式化、数据过滤这些操作如果每次 render 都做很快就成了性能瓶颈。要么在数据源更新时就预处理并缓存要么用 useMemo 按依赖缓存。千万别在 render 里直接写一个.map().filter().reduce()长链条然后每次全量执行。4.4 代码分割与 Suspense首屏快后面才能稳优化不能只盯着运行时体积也是一大部分。一个 React 应用如果把所有页面、所有库都打进一个 bundle首屏加载就得下载几百 KB 甚至上 MB 的 JS白屏时间长这在弱网环境下尤其致命。对了Web 端这个痛点和移动端 React Native 的启动白屏其实是一类问题——核心都是启动时需要执行/加载的内容太多解法思路也相通能延迟的就延迟能拆分的就拆分。代码分割的方案是动态 import。React.lazy 配合 Suspense 可以按路由或按组件懒加载。基础写法const Dashboard React.lazy(() import(./pages/Dashboard)); function App() { return ( Suspense fallback{Loading /} Dashboard / /Suspense ); }实际项目里需要注意的细节有几个一是 Suspense 的边界尽量让 fallback 的粒度小一些只在真正需要加载的子树外面包一层否则一个懒组件加载失败比如网络错误时整个页面的 fallback 一起闪。二是 React.lazy 只适用于默认导出的组件命名导出需要包一层转换。三是结合构建工具的 prefetch 和 preload 策略让用户点击路由之前提前加载点击之后几乎零等待。还有一个常被忽略的点第三方库按需引入。比如用 lodash 就用lodash-es并只 import 用到的函数用 antd 就按需引入组件和样式。Tree shaking 和 sideEffects 配置不处理好打包体积会莫名膨胀。我见过一个项目只是用到了一个日期处理函数结果整个 lodash 被打了进去首屏多了几十 KB原因就是构建配置里的 sideEffects 没有正确开启。5. 一次真实的重渲染问题排查全过程5.1 问题表象页面操作越来越卡越用越慢说一个我去年实际遇到的项目场景。那是一个数据中台系统页面结构左侧导航、顶部工具条、中部是复杂的表格和图表。用户反馈在顶部工具条里切换筛选条件后表格数据更新非常慢而且页面整体有种黏滞感点哪里都像有延迟。我打开 Chrome DevTools 的 Performance 面板录制了一段操作发现单次筛选切换主线程占用接近 800ms其中大量时间消耗在 React 的 re-render 阶段渲染组件数量超过上千个。一眼望去就知道这不是某一道算法卡住的问题而是所有组件全被殃及了。5.2 用 Profiler 一步步缩小范围我改用 React DevTools 的 Profiler 来定位。先开着 Profiler 做一次筛选操作然后看火焰图里哪些组件被标记为渲染。结果触目惊心几乎从头到尾所有组件都亮了一遍包括左侧导航、顶部工具条这种和表格数据毫无关系的组件也在重新 render。这时候就发现了两个问题。第一个表格数据的 store 是通过顶层 Context 提供的Provider 挂在根组件上。筛选条件一变store 更新整个 context value 的引用跟着变化所有消费方全部重渲染。第二个顶部工具条的筛选条件是放在工具条自己内部 state 里的但它的某些 props比如一个 options 数组是在父组件 render 时用.filter()现场创建的导致工具条内部那些 memo 过的子组件也全部失效。这个案例里通信和设计的问题交织在一起正好印证了我前面说的很多性能问题的根源在组件通信方式Context 范围太大、回调引用不稳定和组件设计把不相关数据放在同一作用域上。5.3 修复方案与最终效果修复工作分几步做。第一把 Provider 从根组件往下挪放到真正需要共享数据的页面容器上减小传播范围。第二拆分 Context——表格数据、用户信息、主题设置各成一个 Context互不干扰。第三对工具条组件内部做改造把 options 数组的创建放到 useMemo 里依赖只是筛选值本身回调函数用 useCallback 稳定引用。第四表格容器本身包一层 React.memo。改完之后用同样的操作再录一次 Performance单次筛选主线程占用从 800ms 降到了 120ms 左右渲染组件数从上千个减少到几十个。用户反馈黏滞感完全消失。这次排查给我的教训很深优化性能之前先追溯这个消息是从哪发出、经过了哪些组件、影响到了哪些消费者把通信路径和渲染范围画清楚比盲目加 memo 有用一百倍。6. 关于性能优化我最后想说的几句话玩过一阵子 React 性能优化之后我的最大体会是优化不是冲刺跑而是登山。你不能指望一次大改就把所有问题解决因为业务是活的组件在持续增加通信关系在持续变化渲染路径随时会冒出新的问题。你也不该过早地给每一行代码套上 memo 和 useMemo那样代码可读性会被毁掉真正的问题并不会因为缓存变多而消失。我更推荐的做法是把性能意识内化到日常编码习惯里状态尽量放在局部Provider 尽量靠近消费方传给子组件的引用尽量稳定列表 key 用业务主键数据流向尽量单一清晰。这些习惯不费多少精力却能从源头挡住大部分性能问题。等真正遇到卡顿再用 Profiler 去定位、用各种优化手段去修复这样才不会把代码改得面目全非。如果你正在准备面试这篇文章里涉及的点——组件设计模式、通信方案、渲染优化的原理——恰好是可以串成一条线来讲的知识体系。面试官问 React 性能优化别只背 memo 是什么试着从状态放哪儿、消息怎么传、渲染范围多大这个角度去回答会让你的思路显得成熟很多。同样的逻辑也适用于很多手写 React 实现的考题理解了渲染协调和通信机制手写一个简化版 React 其实就是在复原这套骨架。最后分享一个我自己一直在用的小技巧在代码评审时凡是看到新创建的引用被传给有 memo 的子组件这类写法我都会直接指出来并让作者改成稳定引用。这样做半年后团队的页面性能问题明显少了很多。性能优化说到底拼的就是这些不起眼的细节它们累积起来就是体验的天壤之别。