尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sanity 仓库中的 Vercel React 最佳实践:用 useRef 存储瞬态值,消除高频更新引发的重渲染

Sanity 仓库中的 Vercel React 最佳实践:用 useRef 存储瞬态值,消除高频更新引发的重渲染 Sanity 仓库中的 Vercel React 最佳实践用 useRef 存储瞬态值消除高频更新引发的重渲染【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity本文围绕 Sanity monorepo 内置的 Vercel React 最佳实践规则rerender-use-ref-transient-valuesUse useRef for Transient Values展开先完整继承该规则文档的核心判定标准与正反代码示例再结合 React 的渲染调度机制与 Sanity Studio 源码中真实存在的Resizer拖拽组件讲透为什么用 ref 存瞬态值就能避免重渲染以及该模式的适用边界帮助你在开发内容工作台content workspace类高密度交互应用时做出正确的 state/ref 取舍。一、规则定位Vercel React 最佳实践体系中的一条 MEDIUM 级重渲染规则该规则文件位于仓库的 rerender-use-ref-transient-values.md是 Sanity 仓库为 AI Agent 内置的vercel-react-best-practices技能skill的一部分。同一内容在 skills/vercel-react-best-practices/rules/ 下存在一份逐字节相同的镜像副本已用diff确认两份文件完全一致skills-lock.json记录了该技能集的同步来源信息。规则文件的 YAML frontmatter 给出了它的元数据--- title: Use useRef for Transient Values impact: MEDIUM impactDescription: avoids unnecessary re-renders on frequent updates tags: rerender, useref, state, performance ---从 SKILL.md 可以看到该技能集共包含57 条规则、8 个类别并按影响优先级排序。本规则属于第 5 类Re-render Optimization重渲染优化影响级别为MEDIUM规则摘要为Use refs for transient frequent values为高频瞬态值使用 ref。完整编译文档见 AGENTS.md其中该规则对应第 5.12 节Use useRef for Transient Values。值得注意的是AGENTS.md 开头明确声明这份文档主要是给 Agent 和 LLM 在维护、生成或重构 React/Next.js 代码库时遵循的规范人类读者同样可用但优化目标是自动化与一致性。二、规则核心state 管 UIref 管瞬态值规则文档给出的判定标准一句话概括就是当一个值变化频繁、而你又不希望每次更新都触发重渲染例如鼠标追踪器、定时器、瞬态标志位时把它存进useRef而不是useState。组件状态state留给 UIref 留给临时性的、贴近 DOM 的值。更新 ref 不会触发重渲染。瞬态值transient value的典型场景在文档中明确列出三类mouse trackers—— 鼠标位置追踪mousemove 事件频率可达每秒数十上百次intervals—— 定时器等周期性产生的中间值transient flags—— 交互过程中的临时标志位如正在拖拽。这类值的共同点是它们服务于 DOM 相邻DOM-adjacent的计算而不是驱动组件的 UI 输出。用useState保存它们每次更新都会走 React 的调度、重渲染与协调reconciliation流程成本与鼠标移动频率成正比属于典型的高频更新 × 无 UI 收益的浪费。错误示例每次 mousemove 都触发重渲染规则文档给出的反例是一个追踪鼠标横坐标的Tracker组件function Tracker() { const [lastX, setLastX] useState(0) useEffect(() { const onMove (e: MouseEvent) setLastX(e.clientX) window.addEventListener(mousemove, onMove) return () window.removeEventListener(mousemove, onMove) }, []) return ( div style{{ position: fixed, top: 0, left: lastX, width: 8, height: 8, background: black, }} / ) }问题在于setLastX(e.clientX)使lastX成为响应式状态每收到一次mousemoveReact 都要调度一次重渲染——组件函数体重新执行、div的 props diff 重新计算、样式重新提交到 DOM。对于以 60Hz 甚至更高频率触发的鼠标事件这意味着持续不断的渲染开销而组件树中其他依赖它的部分也连带被牵连。正确示例ref 存值 直接写 DOM全程零重渲染规则文档给出的正例把两件事都做命令式了function Tracker() { const lastXRef useRef(0) const dotRef useRefHTMLDivElement(null) useEffect(() { const onMove (e: MouseEvent) { lastXRef.current e.clientX const node dotRef.current if (node) { node.style.transform translateX(${e.clientX}px) } } window.addEventListener(mousemove, onMove) return () window.removeEventListener(mousemove, onMove) }, []) return ( div ref{dotRef} style{{ position: fixed, top: 0, left: 0, width: 8, height: 8, background: black, transform: translateX(0px), }} / ) }这个正例包含两个配合使用的技巧lastXRef.current e.clientX把最新值写进 ref。useRef返回的对象在组件整个生命周期内是同一个引用写它的.current字段对 React 完全不可见——不经过调度器不产生更新队列条目因此不触发任何重渲染。node.style.transform ...既然位置变化不影响 React 树就直接通过dotRef拿到的 DOM 节点命令式地写transform连样式提交也绕开了 React。初始渲染时left: 0transform: translateX(0px)提供稳定的初始布局之后的位置更新完全发生在 React 渲染循环之外。三、原理剖析为什么写 ref 不会触发重渲染从 React 的运行模型看结合源码结构可以推断useState的更新会创建一个更新对象挂入 fiber 的更新队列并调用ensureRootIsScheduled调度一次渲染而useRef的返回值只是一个挂载在 fiber 上的普通可变对象{current}。对ref.current的赋值是普通 JS 属性写入React 没有任何订阅机制感知它因此不存在标记脏 fiber → 重新执行组件函数的链路。这带来一条必须记住的边界ref 不是响应式的。你不能在 render 函数里读ref.current并期望值变了 UI 也跟着变——ref 变化不会导致组件重新执行。这正是规则文档强调Keep component state for UI的原因值需要反映到界面输出文本、条件分支、列表内容→ 用useState值只服务于事件回调里的计算或直接 DOM 操作→ 用useRef。正例中lastXRef实际上只承担了记录最新位置的簿记角色例如供后续逻辑读取真正的视觉反馈由直接写 DOM 完成。两者缺一不可若只存 ref 却不写 DOM界面上将看不到任何变化若用 state 存值就退化回错误示例的高频重渲染。四、Sanity 源码印证Studio 中的 Resizer 拖拽正是这个模式这条规则不是纸面理论——Sanity Studio 自身的代码就在用它。Resizer.tsx 是 Studio 面板布局中的拖拽分隔条组件它的拖拽处理与本规则的正例几乎同构const mouseXRef useRef(0) // 第 77 行瞬态值用 ref const handleMouseDown useCallback( (event: MouseEvent) { event.preventDefault() mouseXRef.current event.pageX // 第 83 行按下时记录起点 onResizeStart() const handleMouseMove (e: globalThis.MouseEvent) { e.preventDefault() onResize(mouseXRef.current - e.pageX) // 第 89 行拖拽中只算 delta } const controller new AbortController() const handleMouseUp () controller.abort() const {signal} controller window.addEventListener(mousemove, handleMouseMove, {signal}) window.addEventListener(mouseup, handleMouseUp, {signal}) }, [onResize, onResizeStart], )对照规则文档逐条印证mouseXRef useRef(0)第 77 行记录鼠标按下的横坐标这个值在拖拽期间反复被读取但从不出现在任何 JSX 输出里是标准的瞬态值mousemove 处理逻辑放在事件回调而非 effect 依赖里第 87–90 行handleMouseMove每次只计算mouseXRef.current - e.pageX的位移量并回调给父组件Resizer自身在拖拽过程中零重渲染用AbortController的signal管理监听器生命周期第 92–97 行mouseup时abort()一次性移除 mousemove/mouseup 两个监听等价于规则正例中useEffect清理函数的职责只是把清理时机绑定到了交互结束。同一仓库中还有多个组件采用相同的 mousemove 命令式/回调模式处理高频指针事件例如 PaneDivider.tsx、PanelResizer.tsx、Panels.tsx 与 CommandList.tsx从源码结构看高频指针事件不走 state是 Studio 布局交互的一致约定。五、适用边界与配套规则使用本规则时需注意几个边界条件ref 的值不能在渲染中被读出来展示。若某值变化后必须反映到 UI比如显示当前坐标的数字就必须回到 state再考虑用startTransition等机制降级渲染优先级——这属于技能集中rerender-transitionsUse transitions for non-urgent updates的范畴。与rerender-move-effect-to-event联动。本规则的正例本质是把交互逻辑放进事件处理器两条规则经常组合出现mousemove 的响应逻辑写在addEventListener回调里而不是通过 state 中转再在 effect 中消费。与advanced-event-handler-refs联动。若事件处理器本身每轮渲染都重建可把 handler 存入 ref 以保持引用稳定进一步减少 effect 的重绑定。与client-passive-event-listeners联动。规则文档反例的mousemove监听未使用{passive: true}因为它调用了状态更新对 scroll/touchmove 等不preventDefault的监听器应显式声明 passive 以避免浏览器阻塞滚动。在 AGENTS.md 的编译目录中本规则被编排为 Re-render Optimization 章节的第 5.12 条与 5.11Use Transitions for Non-Urgent Updates相邻两者共同构成非紧急、高频更新的两条处理路径还能反映到 UI 的走 transition 降级不需要反映到 UI 的走 ref 命令式 DOM。六、如何把这条规则用到你自己的开发流程给 Agent 用该技能集被放在.agents/skills/下供 AI 编码助手在写新 React 组件、做性能评审、重构现有代码时自动参照SKILL.md 的 When to Apply 一节列出了触发场景。当 Agent 生成类似鼠标追踪、拖拽、定时刷新的组件时这条 MEDIUM 级规则会引导它产出 ref 版本而非 state 版本。给开发者用判断标准可直接复用文档中的一句话——值变化频繁且不需要每次更新都重渲染就选useRef值需要驱动 UI就选useState。落地时参照本文第二节的正例骨架useRef存值 useRef拿 DOM 节点 事件回调里直接写样式初始渲染给出稳定的默认样式。在 Sanity 生态内如果你为 Studio 编写插件或自定义组件如自定义 desk tool、panes处理拖拽、悬停追踪、视口滚动这类高频指针交互时参照 Resizer.tsx 的写法ref 存瞬态值 AbortController 管理监听生命周期是与现有代码库风格一致的实践。小结rerender-use-ref-transient-values这条规则的技术内核非常聚焦——把频繁变化但不驱动 UI的值从响应式 state 迁移到不可观察的 ref并配合命令式 DOM 写入把高频更新的成本从 React 渲染循环彻底移除。它的影响级别是 MEDIUM但恰是内容工作台这类交互密集应用中体感流畅度的关键来源之一。【免费下载链接】sanitySanity Studio – Rapidly configure content workspaces powered by structured content项目地址: https://gitcode.com/GitHub_Trending/sa/sanity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表