mcpField
R
react-performance

react-performance

github.com/robsonrung开发241 次安装今天更新
代码仓库

关于 react-performance

面向 React 开发的建议与代码审查技能,聚焦性能和常见模式。它先识别项目的 React 版本及所用库,再针对适用问题给出代码位置和具体修复建议。

主要功能

  1. 01提供编写建议与代码审查模式
  2. 02检查多余渲染和无效记忆化
  3. 03审视 Context、ref 与过期闭包
  4. 04检查异步请求与竞态条件
  5. 05结合 React 版本和项目库判断

适用场景

  1. 01编写 React 代码前选择组件结构
  2. 02审查变更中的多余渲染
  3. 03查找过期依赖或闭包问题
  4. 04检查请求瀑布流和竞态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/robsonrung/rar-skills --skill react-performance

安装后在 Claude Code 中输入/react-performance确认已加载。

附带文件references/cheatsheet.md

详细介绍

React 性能与模式分析

根据 Nadia Makarevich 的《Advanced React》中的规则,为 React 代码提供建议并进行审查。 它只关注一组问题:是否发生了不必要的重新渲染,记忆化是否真的起作用,闭包是否过期,以及异步流程是否安全? 这不是用于排查逻辑错误的漏洞搜寻——这类问题应通过代码审查来处理。

两种模式,根据上下文选择:

  • 顾问(编写新的 React 代码时):在编写代码之前引导决策——优先选择组合而非 memo,正确放置状态,并妥善组织数据获取。
  • 审查者(审查现有代码或变更后的代码时):根据下方的检查项进行审计,按分析维度归类报告发现。引用 file:line,注明规则,并提出具体修复方案。跳过不适用的维度,不要为了凑数而添加内容。如果组件没有问题,就直说。

先检测目标(应用规则前先阅读)

以下每个分析维度都适用于 React 17 及更高版本。不同版本之间存在少数具体规则差异——因此要先检测,切勿想当然:

  1. React 版本。 阅读目标仓库的 package.json(react / react-dom,范围较宽松时也要查看锁文件)。记下主版本号。
  2. 状态库。 查看是否使用外部 store——Redux Toolkit、Zustand、Jotai、MobX、TanStack Query、SWR、Apollo——以及它是否已经负责管理服务端状态。
  3. UI 库。 查看是否使用 MUI、Chakra、Radix、Headless UI、Ant、shadcn/ui、仅使用 Tailwind,或手写组件。

然后根据检测结果应用各个分析维度,并明确指出版本特定规则,而不是断言某个版本的行为适用于所有版本:

  • Refs 和 forwardRef。 React 19 之前,函数组件必须使用 forwardRef 才能接收 ref。从 React 19 开始,ref 是普通 prop,不再需要 forwardRef。标记任一写法有误之前,先确认检测到的版本。
  • 自动批处理。 React 17 只在 React 事件处理函数内进行批处理;React 18 及更高版本则会在任何地方批处理。“为什么渲染了两次”这一发现取决于你使用的版本。
  • 指明发现所依据的版本:“在检测到的 React 17 版本中,……”胜过单独陈述规则。

两个反复出现、由检测结果决定的判断:

  • 深层且对重新渲染敏感的组件树(常见于大量使用 styled-component 的库,例如 MUI)会让不必要的父组件重新渲染代价高昂——级联更新会经过每一个样式包装组件。检测到这类情况时,提高分析维度 1 的权重。
  • 已有带记忆化选择器的外部 store 时,跨组件树共享状态应优先使用其选择器,而非手写 Context;Context 应用于低频、局部的配置(主题、当前实体),而非高频变化的状态。若没有这类 store,则分析维度 3 中关于 Context 的建议就是完整答案。

这本书出版于 2023 年,与 React 17/18 高度契合;如果检测到的版本更新,则优先采用该版本框架文档中记录的行为,而非沿用书中的假设。

黄金法则(适用于以下所有分析维度)

优先组合,最后才考虑记忆化。React.memo 是组合技巧无法解决问题后的最后手段——因为要正确记忆化所有 props 比看起来更难,只要漏掉一个非原始类型的 prop,就会悄无声息地让它失效。


视角 1 — 不必要的重新渲染(第 1–4 章)

状态更新会重新渲染持有该状态的组件及其所有子组件,不论 props 如何。只有使用 React.memo 后,props 才会发挥作用。因此,成本最低的修复方式是调整结构,而不是使用记忆化。

按以下顺序检查:

  1. 下移状态。 快速变化的状态(输入值、悬停标记、打开/关闭开关)是否放在了组件树的高层,导致大型子树重新渲染?将状态和使用它的小部分内容提取到一个子组件中。这是本书中价值最高的一项修复。
  2. 将子组件 / 元素作为 props 传入。 如果组件持有状态,却渲染了不依赖该状态的复杂子树,就将该子树作为 children(或元素 prop)传入。作为 props 传入的元素在父组件自身状态改变时不会重新渲染——它们是由父组件的父组件创建的。具体模式:速查表第 2 章。
  3. 只有当子组件确实需要父组件的状态 / DOM 数据时,才使用渲染属性(例如,将逻辑绑定到 DOM 元素)。Hooks 已取代约 99% 过去“共享有状态逻辑”的使用场景——不要只为了共享逻辑就使用渲染属性。

危险信号:顶层页面组件持有输入值;provider 或 layout 中的 useState 包裹了半个应用;把“用 memo 包起来”当作首选方案。

视角 2 — 毫无作用的记忆化(第 5 章)

React 在 memo props 和 Hook 依赖项中,按引用比较对象 / 数组 / 函数。useMemo 会记忆化结果;useCallback 会记忆化函数本身。

标记没有带来收益的记忆化——只有满足以下任一条件时,才有理由使用 useMemo / useCallback:

  • 该值是另一个 Hook(useEffect/useMemo/useCallback)的依赖项,或者
  • 该值作为 prop 传给了由 React.memo 包装的组件,或者
  • 该值传给了某个组件,而该组件随后满足上述任一条件。

还要标记被悄然抵消的 React.memo:

  • 传给记忆化组件的非基本类型 prop(对象 / 数组 / 函数 / children)每次渲染都会重新创建 → 记忆化毫无用处。所有 props 都必须是基本类型或保持稳定。children 也是 prop,很容易忘记对它进行记忆化。
  • 该值来自另一个未记忆化的 prop 或 Hook 结果 → 记忆化链断裂。

经验法则:如果你在记忆化某个 prop,就要证明使用它的组件由 React.memo 包装,或将它用作依赖项。否则就删除这层记忆化。

记忆化的列表项,或状态在渲染间神秘地重置 / 保留 → 检查 keys 和协调过程,参见速查表第 6 章。

视角 3 — Context 性能(第 8 章)

Context 的值发生变化时,每个消费者都会重新渲染——而且标准记忆化无法阻止这一点。

  • 标记值为 value={{...}} / value={[a, b]} 的 provider 是否使用了未记忆化的内联对象 / 数组——每次父组件渲染都会迫使所有消费者重新渲染。使用 useMemo 包装(并使用 useCallback 包装回调)。
  • 对于多个互不相关的值,拆分成多个 provider,这样一个值变化时就不会让使用另一个值的消费者重新渲染。useState → useReducer 有助于将数据和 API 保存在各自稳定的 Context 中。
  • Context 没有真正的选择器;你可以用 React.memo + HOC 模拟(参见速查表第 7 章),但如果发现自己在这么做,就改用仓库中已有的外部存储(见上文检测方法)——真正的选择器 API 胜过模拟实现。

Lens 4 — Refs、闭包与陈旧状态(第 9–11 章)

  • ref 是一个在多次渲染间保留的可变容器;修改 ref.current 不会触发重新渲染,而且是同步的。对于必须持久保留、但不应触发渲染的值(计时器、最新回调、先前的值、DOM 节点),请使用它。不要将它用于任何应该显示在 UI 中的内容。
  • forwardRef 在 React 19 之前是将 ref 属性传递给函数组件的必需方式;从 React 19 起,ref 是普通属性。标记问题之前,请检查检测到的版本。无论哪种情况,都应使用 useImperativeHandle 暴露受控的命令式 API,而不是泄露 DOM 节点。
  • 陈旧闭包 = 此处最常见的错误。在渲染期间创建的函数会“冻结”它捕获的状态和属性。如果它使用了缺少依赖项的 useCallback/useMemo 进行记忆化,或只存入 ref 一次,就会一直读取旧值。
    • 检测:useCallback/useEffect/useMemo 读取了状态/属性,但依赖项中缺少它们;存储在 ref.current 中且从未更新的回调。
    • 解决:在每次渲染时通过 useEffect 将闭包函数更新到 ref.current 中,然后调用 ref.current()——这样它总能读取最新数据。这也是保持 React.memo 子组件稳定、同时仍能调用最新逻辑的方法。

Lens 5 — 防抖 / 节流(第 11 章)

只有在组件的整个生命周期内都使用同一个实例时,debounce/throttle 才能正常工作。

  • 标记在渲染中内联创建的 debounce(...) / throttle(...) / setTimeout 定时器 ID,或在未记忆化的处理函数中创建的这些内容——每次渲染都会重新创建定时器,导致其无法正常触发。
  • 修复:将防抖函数记忆化一次(useMemo/ref),并通过 Lens 4 中的 ref 更新技巧获取最新状态(直接记忆化会将创建时的状态冻结)。

Lens 6 — UI 闪烁 / useLayoutEffect(第 12 章)

在 useEffect 中执行测量后再修改 DOM(尺寸/位置),会让浏览器先绘制修改前的画面 → 造成明显的闪烁。使用 useLayoutEffect——它会在绘制前同步运行。SSR 注意事项和精确模式:速查表第 12 章。

Lens 7 — Portal 与堆叠上下文(第 13 章)

如果模态框/工具提示/下拉菜单被祖先元素的 overflow 裁剪,或被困在堆叠上下文中(任何内容都无法逃出,包括 position: fixed),就应通过 Portal 渲染。大多数组件库的覆盖层原语已使用 portal(MUI Modal/Popper、Radix、Headless UI)——请检查检测到的库,然后标记手动实现但未使用 portal 的覆盖层。CSS 规则和事件冒泡行为:速查表第 13 章。

Lens 8 — 数据获取:瀑布流与竞态条件(第 14–15 章)

  • 瀑布流: 本可以并行执行的 await 却按顺序或有条件地执行。标记相互依赖的 useEffect 数据获取,以及 a 和 b 互不依赖时的 await a; await b → 使用 Promise.all、并行 Promise 或数据提供者 Context。注意浏览器的并行请求限制;关键资源可以在 React 挂载前预获取。
  • 竞态条件: 在依赖会变化的值(例如 url、某个 ID)的 effect 中,于 await/.then 之后调用 setState。较早发出的慢速请求可能在较新的请求之后才完成,覆盖最新数据。修复方法(优先考虑最后两种):在 setState 前比较已解析的 ID;在 useEffect 中使用清理标记;AbortController。精确模式:速查表第 15 章。(服务端状态数据层——RTK Query、TanStack Query、SWR、Apollo——可以为你处理这些问题;如果代码库中已有此类数据层,处理服务端状态时应优先使用它,而不是在 effect 中手动实现 fetch。)

Lens 9 — 错误处理(第 16 章)

React 16 之后,未捕获的渲染错误会卸载整个应用——在关键位置设置几个 ErrorBoundary 是必不可少的。边界无法捕获回调/setTimeout/Promise 中的错误;try/catch 无法捕获渲染/useEffect/嵌套组件中的错误。可通过将异步错误重新抛入渲染生命周期来衔接,或使用 react-error-boundary 库。精确的 Hook 和覆盖范围矩阵:速查表第 16 章。


如何应用

  1. 确定这项改动涉及哪些视角(fetch hook → 8;provider → 3;使用 memo 的 组件 → 1、2、4)。
  2. 在 advisor 模式下,优先推荐结构性方案(下移状态 / 组合),只有满足视角 2 中的检查条件后,才考虑使用 memo。
  3. 在 reviewer 模式下,按视角报告:file:line → 对应规则 → 具体修复方法。
  4. 不要臆测无关紧要的重复渲染——渲染开销低且不频繁的组件无需优化。针对状态频繁变化且子树开销大的部分进行优化。

请参阅 references/cheatsheet.md,了解各章节的决策表和代码片段。

评论

支持 Markdown