OpenMontage 前端性能实战:遵循 Vercel React 最佳实践,在渲染期间计算派生状态而非依赖 useEffect
2026/9/11 5:32:29 网站建设 项目流程

OpenMontage 前端性能实战:遵循 Vercel React 最佳实践,在渲染期间计算派生状态而非依赖 useEffect

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

导读

本文深入剖析 Vercel React 最佳实践中「在渲染期间计算派生状态(Calculate Derived State During Rendering)」这一核心规则,它属于重渲染优化(Re-render Optimization)范畴,用于消除因冗余 state 与 effect 联动带来的额外渲染和状态漂移(state drift)。在本仓库中,该规则被收录于 .claude/skills/vercel-react-best-practices 与 .agents/skills/vercel-react-best-practices 两个 Agent Skill 目录(同一份技能以双份部署),并被rerender-前缀下的多条姊妹规则共同支撑。读完本文,你将掌握如何识别「可以用派生值取代的冗余 state + effect」,写出更少重渲染、无状态漂移的 React/Remotion 组件,并能将同类审查能力直接应用到本仓库 remotion-composer 的视频合成器前端代码上。

规则原文与定位

规则文件 rerender-derived-state-no-effect.md 的 frontmatter 明确定义了它的属性:

--- title: Calculate Derived State During Rendering impact: MEDIUM impactDescription: avoids redundant renders and state drift tags: rerender, derived-state, useEffect, state ---
  • 标题:在渲染期间计算派生状态;
  • 影响等级:MEDIUM(中等收益),对应技能体系中的「5. Re-render Optimization」分类;
  • 影响描述:避免冗余渲染与状态漂移;
  • 标签:rerender / derived-state / useEffect / state。

在技能的整体分类体系中(见 SKILL.md 与 rules/_sections.md),8 大分类按优先级排列,本规则属于第 5 类「Re-render Optimization(重渲染优化)」,该类别的核心目标描述为:Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness(减少不必要的重渲染以最小化浪费的计算并提升 UI 响应性)。

规则的核心主张可以用一句话概括:

如果一个值可以由当前的 props/state 直接计算得出,就不要把它存进 state,也不要用 effect 去更新它。应当在渲染期间派生出该值,从而避免额外渲染和状态漂移。不要仅仅为了响应 prop 变化而在 effect 里 setState;应优先使用派生值,或使用 keyed resets(基于 key 的重置)作为替代方案。

这条规则与 React 官方文档「You Might Not Need an Effect」一脉相承,其本质是:effect 应保留给真正需要与外部系统同步的场景(订阅、网络请求、DOM 操作等),而不应承担「在 state 之间搬运数据」的工作

反模式:冗余 state 与 effect 的联动

规则文档给出了一段典型反模式代码:

function Form() { const [firstName, setFirstName] = useState('First') const [lastName, setLastName] = useState('Last') const [fullName, setFullName] = useState('') useEffect(() => { setFullName(firstName + ' ' + lastName) }, [firstName, lastName]) return <p>{fullName}</p> }

这段代码存在的问题可以从三个层面拆解:

  1. 引入了一次额外的渲染fullName被存成 state 后,当firstNamelastName变化时,React 需要经历「渲染 → 提交 → 触发 effect → 再次 setState → 再次渲染」的完整闭环。也就是说,一次用户输入至少要触发两轮渲染,而fullName本身在第二轮之前显示的还是一份过期值。
  2. 存在中间态 / 状态漂移风险:在 effect 触发之前的那个渲染帧里,fullName仍是旧值,UI 会短暂显示与当前输入不一致的内容。在更复杂的场景中(例如多个 state 相互派生的链式 effect),中间态会进一步放大,甚至形成难以追踪的级联更新。
  3. 依赖数组是隐性契约:effect 的正确性完全依赖开发者把[firstName, lastName]写全。一旦将来组件新增了影响fullName计算的第三个输入而忘记更新依赖数组,就会产生引用过期 state 的陈旧闭包(stale closure)——这正是同技能下 rerender-functional-setstate.md 所警告的那类 bug 源头。

正解:在渲染期间直接派生

同一份规则给出了修正版本:

function Form() { const [firstName, setFirstName] = useState('First') const [lastName, setLastName] = useState('Last') const fullName = firstName + ' ' + lastName return <p>{fullName}</p> }

修正后的关键差异在于:fullName只是一个普通局部变量,在每次渲染时由当前的firstNamelastName同步计算得出。由此获得三方面收益:

  • 零额外渲染:输入变化只需一轮渲染,UI 永远与输入状态同步,不存在中间帧;
  • 零状态漂移:派生值在数学上恒等于「当前状态的函数」,任何时刻渲染结果都与状态严格一致,不会出现「状态已变、界面未跟上」的窗口期;
  • 零依赖数组:没有 effect,就不存在依赖数组漏写、闭包过期的问题,代码心智负担显著降低。

派生计算本身需要遵循「轻量」原则——它会在每次渲染时执行,因此适用于字符串拼接、布尔判断、数组 filter/map、对象重组等低成本运算;如果派生计算开销高昂(如大型搜索索引构建),则应参考 rerender-memo.md 用useMemo按依赖缓存,或参考 rerender-lazy-state-init.md 将一次性重计算放入useState的惰性初始化函数。

何时不能直接派生:关于 prop 变化重置 state 的替代方案

规则明确指出,当「派生值」无法表达可修改的受控状态(例如表单正在编辑、但尚未提交的半成品数据),或需要在 prop 变化时重置一段内部 state时,不能简单照搬派生公式。此时规则给出两个替代方案:

方案一:keyed resets(基于 key 的重置)

当某个 state 完全从属于某个 prop 时,与其在 effect 里监听 prop 变化后setState,不如直接利用 React 的key机制强制重建组件:

function ProfilePage({ userId }) { return <ProfileForm key={userId} /> } function ProfileForm({ userId }) { // 每次 userId 变化,整个组件被重新挂载,内部 state 自动重置 const [draft, setDraft] = useState(loadDraft(userId)) // ... }

key={userId}让 React 在userId变化时销毁旧组件实例并挂载新实例,内部所有useState都天然回到初始值——比「effect 里手动同步 + 手动重置」更少代码、更少出错机会,也彻底消除了「旧 key 的新状态残留在界面上」的漂移窗口。

方案二:在渲染期间基于上一轮值调整 state

若确实需要保留可编辑状态,官方推荐的「在渲染期间调整 state」模式同样属于「不用 effect 响应 prop」的范畴。其要点是在渲染函数体内比较上一轮渲染记录的值,并在渲染过程中调用 setter(React 会立即用新值重新渲染该组件,不会等待 effect):

function List({ items }) { const [prevItems, setPrevItems] = useState(items) const [selection, setSelection] = useState(null) if (items !== prevItems) { setPrevItems(items) setSelection(null) // 在渲染期间重置派生选择态 } // ... }

注意该模式只适用于「在渲染期间调整 state」的场景,且应当放在条件分支中、避免无条件 setState 造成无限循环。setPrevItems用于记录基线,setSelection完成重置,整个过程不经过 effect、不会额外多出一轮提交。

联动规则:事件里的逻辑不要建模成 state + effect

与「prop 变化触发 effect」类似的另一种常见反模式是「用户动作建模成 state + effect」。姊妹规则 rerender-move-effect-to-event.md 指出:如果副作用由具体的用户动作(提交、点击、拖拽)触发,就应当直接写在事件处理器里,而不是setState(true)+ effect 监听。二者同根同源——凡是「值可以算出来」或「动作可以直接执行」的场景,都不需要 effect 作为中转站

状态漂移的深层机制

「状态漂移」是本规则 impactDescription 的核心词,值得展开解释其成因链条:

  1. 当 A 与 B 两个 state 共同派生 C,而 C 又被存为 state 时,A 或 B 的每一次变化都会让 C 短暂处于「过期」状态,直到 effect 补跑;
  2. 如果派生链更长(C 派生 D,D 派生 E),每一环都额外引入一轮渲染与一个中间帧,形成级联渲染风暴
  3. 并发特性(如startTransition)下,过期值甚至可能跨越优先级边界被用户看到,产生「界面回跳」;
  4. 依赖数组遗漏时,C 会长期停留在旧值上,且没有任何编译期报错——这是最隐蔽的漂移。

从源码结构看,本仓库的 remotion-composer 前端大量采用「渲染期间派生」的模式:以 CalloutBox.tsx 为例,组件并没有把resolvedBorderresolvedBgresolvedIcon存入 state,而是在每次渲染时通过const resolvedBorder = borderColor || defaults.border这类表达式由 props 与TYPE_DEFAULTS常量直接派生;动画值slideXopacityscaleborderDraw等也全部由useCurrentFrame()返回的frame在渲染期间通过spring()/interpolate()现算,而不是把每一帧写入 state。这正是「能算就不存」原则在视频合成器场景下的直接体现——Remotion 的渲染模型要求每一帧都从当前 frame 派生,任何把帧号派生值塞进 state 的做法都会引入多余的提交与中间态。

实战自查清单

将本规则落地为可执行的审查动作,可以在每次写/改 React 组件时对照检查:

  1. 看到useState后先问一句:这个值能由现有 props/state 现算出来吗?能算就删掉 state,直接渲染时派生;
  2. 看到useEffect+setState先问一句:这个 effect 是不是仅仅在「把 prop 同步进 state」?如果是,改用派生值或key重置;
  3. 看到依赖数组就保持警惕:依赖数组的存在意味着一段隐式同步契约,能删则删;
  4. 区分「派生展示值」与「可编辑受控值」:展示性聚合(fullName、合计金额、过滤列表)一律派生;用户可编辑的半成品数据才需要 state;
  5. 派生计算是否轻量:每次渲染都会执行,重计算请配合useMemo、惰性初始化(rerender-lazy-state-init.md)或拆分到子组件并用memo隔离(rerender-memo.md)。

在本仓库中的阅读与使用方式

本技能在仓库内以两份镜像部署,内容一致,分别面向不同 Agent 工具链加载:

  • Claude 工具链入口:.claude/skills/vercel-react-best-practices/SKILL.md,其元数据声明license: MITauthor: vercelversion: "1.0.0",共收录 65 条规则、覆盖 8 大分类;
  • 其他 Agent 工具链入口:.agents/skills/vercel-react-best-practices/SKILL.md;
  • 规则全文:本规则位于.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
  • 同类「重渲染优化」规则:rerender-前缀下共 15 条,覆盖派生状态订阅(rerender-derived-state.md)、事件内联、函数式 setState、惰性初始化、useDeferredValue、临时值放 ref 等互补场景,可在审查时整组对照。

当你在本仓库的 remotion-composer/src/components 下编写或评审新的 React 组件时,可直接以本规则作为性能与正确性的双重标准:凡是能从当前 frame、props、现有 state 计算出来的值,一律在渲染期间派生;effect 只留给真正需要与外部系统同步的工作。这既符合 Vercel 工程团队的审查口径,也与 Remotion 声明式渲染模型天然契合。

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询