关于 react-performance
面向 React 开发的建议与代码审查技能,聚焦性能和常见模式。它先识别项目的 React 版本及所用库,再针对适用问题给出代码位置和具体修复建议。
主要功能
- 01提供编写建议与代码审查模式
- 02检查多余渲染和无效记忆化
- 03审视 Context、ref 与过期闭包
- 04检查异步请求与竞态条件
- 05结合 React 版本和项目库判断
适用场景
- 01编写 React 代码前选择组件结构
- 02审查变更中的多余渲染
- 03查找过期依赖或闭包问题
- 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 及更高版本。不同版本之间存在少数具体规则差异——因此要先检测,切勿想当然:
- React 版本。 阅读目标仓库的
package.json(react/react-dom,范围较宽松时也要查看锁文件)。记下主版本号。 - 状态库。 查看是否使用外部 store——Redux Toolkit、Zustand、Jotai、MobX、TanStack Query、SWR、Apollo——以及它是否已经负责管理服务端状态。
- 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 才会发挥作用。因此,成本最低的修复方式是调整结构,而不是使用记忆化。
按以下顺序检查:
- 下移状态。 快速变化的状态(输入值、悬停标记、打开/关闭开关)是否放在了组件树的高层,导致大型子树重新渲染?将状态和使用它的小部分内容提取到一个子组件中。这是本书中价值最高的一项修复。
- 将子组件 / 元素作为 props 传入。 如果组件持有状态,却渲染了不依赖该状态的复杂子树,就将该子树作为
children(或元素 prop)传入。作为 props 传入的元素在父组件自身状态改变时不会重新渲染——它们是由父组件的父组件创建的。具体模式:速查表第 2 章。 - 只有当子组件确实需要父组件的状态 / 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 章。
如何应用
- 确定这项改动涉及哪些视角(fetch hook → 8;provider → 3;使用 memo 的 组件 → 1、2、4)。
- 在 advisor 模式下,优先推荐结构性方案(下移状态 / 组合),只有满足视角 2 中的检查条件后,才考虑使用 memo。
- 在 reviewer 模式下,按视角报告:
file:line→ 对应规则 → 具体修复方法。 - 不要臆测无关紧要的重复渲染——渲染开销低且不频繁的组件无需优化。针对状态频繁变化且子树开销大的部分进行优化。
请参阅 references/cheatsheet.md,了解各章节的决策表和代码片段。