open-agents 中的 React 性能规则实践:窄化 useEffect 依赖项,最小化副作用重跑(rerender-dependencies)
【免费下载链接】open-agentsAn open source template for building cloud agents.项目地址: https://gitcode.com/GitHub_Trending/op/open-agents
open-agents 仓库内置了一份面向 AI Agent 与 LLM 的 Vercel React 最佳实践规则库,其中rerender-dependencies规则规定了如何为useEffect声明依赖项,把副作用的重跑次数压到最低。读完本文,你会理解该规则在整套 React/Next.js 性能规则体系中的定位、依赖比较的底层机制,掌握对象依赖、派生条件依赖的三类典型改写方式,以及它与其他重渲染优化规则如何协同、适用边界在哪里,从而在 Next.js 客户端组件中稳定写出「只在真正需要的值变化时才执行」的副作用。
1. 规则定位:rerender 类别中的一环
该规则的原始文档位于 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md,其 frontmatter 元数据为:
- title:
Narrow Effect Dependencies(窄化 Effect 依赖) - impact:
LOW(影响等级:低) - impactDescription:
minimizes effect re-runs(最小化 effect 重跑) - tags:
rerender, useEffect, dependencies, optimization
这份规则是技能 .agents/skills/vercel-react-best-practices/SKILL.md 所述「58 条规则、8 个类别」体系的一部分。技能定义了如下按优先级排序的类别框架(引自 SKILL.md):
| 优先级 | 类别 | 影响 | 前缀 |
|---|---|---|---|
| 1 | 消除瀑布式请求(Eliminating Waterfalls) | CRITICAL | async- |
| 2 | 包体积优化(Bundle Size Optimization) | CRITICAL | bundle- |
| 3 | 服务端性能(Server-Side Performance) | HIGH | server- |
| 4 | 客户端数据获取(Client-Side Data Fetching) | MEDIUM-HIGH | client- |
| 5 | 重渲染优化(Re-render Optimization) | MEDIUM | rerender- |
| 6 | 渲染性能(Rendering Performance) | MEDIUM | rendering- |
| 7 | JavaScript 性能(JavaScript Performance) | LOW-MEDIUM | js- |
| 8 | 高级模式(Advanced Patterns) | LOW | advanced- |
从这套体系可以看出两点:其一,本规则属于第 5 类「重渲染优化」(rerender-前缀,整体 MEDIUM 档),而单条规则自身被进一步标记为 LOW——这意味着它不是性能优化的「头号杀手锏」(那是瀑布请求与包体积),而是一类「少跑几次副作用」的增量收益规则;其二,规则文件通过文件名前缀自动归入对应章节,构建时会按标题排序并生成编号。编译后的完整文档见 .agents/skills/vercel-react-best-practices/AGENTS.md,该规则对应其中的5.6 节「Narrow Effect Dependencies」。
规则库的构建与校验流程在 README 中说明:每条规则是一个独立的rules/<前缀>-<描述>.md文件,通过构建脚本编译生成 AGENTS.md 与测试用例文件。对使用者来说,这意味着每条规则都具备独立可读性与可检索性,本规则即可单独作为评审检查项使用。
2. 核心原则:用原始值而非对象做依赖
规则原文一句话概括:
Specify primitive dependencies instead of objects to minimize effect re-runs. (用原始值类型的依赖替代对象依赖,以最小化 effect 重跑。)
2.1 为什么[user]会「误触发」
React 对useEffect的依赖数组做浅比较:每一项与上一次渲染的对应项用Object.is相等性比较(可推断为:字符串、数字、布尔值、null/undefined 等原始值比较结果,对象与数组则比较引用地址)。由此产生规则针对的行为差异:
// 不正确:user 是对象。任何一次 user 引用变化(哪怕只有 email 字段变了) // 都会让依赖比较失败,effect 整体重跑 useEffect(() => { console.log(user.id) }, [user])// 正确:依赖原始值 user.id。只有 id 真正改变时才重跑 useEffect(() => { console.log(user.id) }, [user.id])错误示例中,effect 体内实际只读取了user.id一个字段,却订阅了整个user对象。当上游数据把user替换为一个新对象(例如接口刷新、store 更新)而id并未变化时,[user]版本的 effect 会白白重跑;[user.id]版本则静默跳过。这正是「narrow(窄化)」的含义:依赖声明的范围应当收缩到 effect 体真正消费的字段为止。
2.2 派生条件:先算布尔值,再依赖布尔值
原文给出的第二个例子针对的是「连续值 → 布尔语义」的场景:
// 不正确:width 是连续变化的原始值。宽度从 767 → 766 → 765…… // 每一次变化都会触发 effect 重跑,但业务语义(是否移动端)从未改变 useEffect(() => { if (width < 768) { enableMobileMode() } }, [width]) // 正确:在渲染期先派生出布尔值 isMobile, // 只有布尔值发生 0↔1 跃迁时 effect 才重跑 const isMobile = width < 768 useEffect(() => { if (isMobile) { enableMobileMode() } }, [isMobile])这个示例值得单独展开:width本身已经是原始值,依赖它并不违反「原始值依赖」的字面要求,但业务条件只关心「是否小于 768」这个二值结论。把比较运算上移到渲染期(const isMobile = width < 768),让依赖项退化为布尔值后,effect 的重跑次数从「每个像素变化一次」降为「跨过阈值一次」。这也解释了文档小标题「For derived state, compute outside effect」——派生计算必须放在 effect 之外(即渲染函数体中),否则依赖数组里放不进去尚未计算的中间结果。
3. 与其他 rerender 规则的分工
把本规则放进同族规则里看,边界更清晰(以下均出自同一技能目录的rules/文件):
| 规则文件 | 解决的问题 | 与本规则的关系 |
|---|---|---|
| rerender-derived-state.md | 组件级:订阅派生布尔状态而非连续值(如用useMediaQuery('(max-width: 767px)')替代useWindowWidth()) | 同一思想在组件重渲染层面的应用;本规则是其在effect 依赖层面的应用 |
| rerender-derived-state-no-effect.md | 能渲染期算出的值不要存 state、不要用 effect 同步 | 上游决策:若派生值根本不需要触发副作用,应优先在渲染期直接派生,连 effect 都不留 |
| rerender-defer-reads.md | 仅在回调里读取的 state(如useSearchParams)不要订阅 | 与本规则互补:本规则解决「依赖谁」,它解决「要不要依赖」 |
| advanced-use-latest.md | 用useLatest获得稳定回调引用 | 当 effect 必须读取频繁变化的值但不想因此重跑时,配合本规则使用 |
可以这样归纳三者的决策顺序:
- 能不能不写 effect?若值只是派生展示,按
rerender-derived-state-no-effect在渲染期直接计算; - 能不能不订阅?若只在事件回调里读取,按
rerender-defer-reads在回调内按需读取; - 必须订阅时依赖谁?按本规则把依赖收窄为原始值 / 派生布尔值。
4. 边界与陷阱:窄化不是漏写
窄化依赖项有一个前提:effect 体读取的每一个响应式值都必须被某个依赖覆盖,否则就会读到过期闭包值。原文档聚焦在「多订阅」问题,以下是实践中必须同时守住的边界(基于 React 依赖比较机制的一般性约束,供评审时对照检查):
- 禁止用「窄化」为由删掉真实读取的依赖。若 effect 体同时读取
user.id与user.name,依赖就必须是[user.id, user.name];只写[user.id]会让name变成陈旧值,触发 ESLintreact-hooks/exhaustive-deps告警。 - 对象依赖并非总是错误。若 effect 确实消费对象的多个字段,声明整个对象是诚实且安全的(多跑几次副作用,但永远读到最新值)。本规则的收益只出现在「effect 只消费对象的一小部分字段」的场景。
- 引用不稳定的对象依赖是重灾区。
[props.filters](每次渲染新构造的对象/数组)会让 effect 每次渲染都重跑。处理方式取决于意图:要么按本规则拆出真正用到的原始字段,要么在渲染期用useMemo稳定引用,要么把交互逻辑移出 effect(参见同族规则 rerender-move-effect-to-event.md——「交互触发的副作用应放事件处理器」)。 - 派生值必须在渲染期计算。
const isMobile = width < 768这类语句放在组件函数体内、useEffect调用之前;放进 effect 体内则依赖数组无从引用,问题依旧。 - 与 React Compiler 的关系:编译文档中明确提示,若项目启用了 React Compiler,
memo()/useMemo()之类的手工记忆化不再必要,编译器会自动处理重渲染优化。对 effect 依赖,Compiler 不改变「依赖数组决定 effect 何时重跑」这一机制,但会缓解其中依赖的引用稳定性问题——从编译文档的口径看,该提示主要针对渲染层面的手工记忆化,对 effect 依赖声明仍需开发者自行收窄。
5. 在 open-agents 中的应用语境
open-agents 是一个基于 Next.js App Router 的 monorepo(工作区见 AGENTS.md:Web 应用位于apps/web,另含packages/agent、packages/sandbox等包)。其前端包含大量带客户端状态与副作用的组件(会话列表、聊天界面、设置页等,如 apps/web/components、apps/web/hooks 目录),数据获取普遍走 SWR 封装(见 apps/web/lib/swr.ts),因此「客户端组件中 effect 因对象依赖频繁重跑」是真实存在的代码形态,本规则对该类代码的评审与生成具有直接约束力。
在该仓库落地这条规则时,建议结合其既有工程约定:
- 代码风格:仓库要求双引号、2 空格缩进、Bun 工具链(AGENTS.md 的 Code Style 一节)。改写依赖数组时保持既有风格,并通过
bun run ci(lint + typecheck + tests)与bun run check验证。 - 文件组织:仓库要求「每个文件聚焦单一职责,大组件的新逻辑提取到同目录 hooks/子组件」。这与本规则形成呼应——当一个组件的 effect 依赖越来越杂,往往是该把派生值与副作用拆进独立 hook 的信号;拆出后每个 effect 的依赖面更小,也更容易做到「依赖即原始值」。
- 技能触发场景:SKILL.md 规定该技能在「编写新 React 组件、实现数据获取、性能评审、重构现有代码、优化包体积或加载时间」时应用。即在 Agent 生成或评审 open-agents 中任何
useEffect代码时,应检查:依赖数组里是否存在 effect 体并未消费的整个对象;连续值依赖能否替换为渲染期派生的布尔值。
6. 评审检查清单
对任意一段包含useEffect的 Next.js/React 代码,可按以下顺序执行本规则的检查:
- 列出 effect 体内实际读取的每个响应式值;
- 将依赖数组中每个「对象/数组」依赖展开为其被消费的字段,改为原始值依赖;
- 检查依赖中是否存在「连续值 → 业务条件」的情形,若是,在渲染期派生布尔值并改为依赖布尔值;
- 反向检查:effect 体读取的每个响应式值是否都在依赖数组中(防过期闭包);
- 若依赖项是每次渲染新建的对象/数组引用,判断是拆字段、稳定引用,还是把逻辑移出 effect(转入事件处理器)。
规则本身标注的影响等级为 LOW,属于「少跑几次 effect」的增量优化;但它与同族的 rerender-derived-state.md、rerender-derived-state-no-effect.md、rerender-defer-reads.md 一起构成完整的「订阅最小化」决策链——先问要不要 effect、再问要不要订阅、最后问依赖该多窄——在 open-agents 这类含大量客户端交互状态的 Next.js 应用中,是控制副作用重跑频率的直接手段。
完整规则集及其优先级框架见 SKILL.md 与编译文档 AGENTS.md(5.6 节);规则文件的组织与构建方式见 README。
【免费下载链接】open-agentsAn open source template for building cloud agents.项目地址: https://gitcode.com/GitHub_Trending/op/open-agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考