ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TanStack Router 渲染优化指南:structural sharing 与细粒度选择器实战

TanStack Router 渲染优化指南:structural sharing 与细粒度选择器实战 TanStack Router 渲染优化指南structural sharing 与细粒度选择器实战【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/routerTanStack Router 内置了多项渲染优化机制确保组件只在必要时重新渲染。本文围绕 render-optimizations.md 这一核心指南深入讲解structural sharing结构共享与fine-grained selectors细粒度选择器两大技术包括它们解决什么问题、如何在路由与 Hook 中启用、与 TypeScript 类型系统如何协作以及源码级的实现原理。读完本文你将掌握如何用select精准控制组件订阅范围、用structuralSharing保持引用稳定性从而在高频导航与 URL 状态变化场景下显著减少不必要的重渲染。一、为什么需要渲染优化URL 即状态在 TanStack Router 中URL尤其是 search 参数就是应用状态的一部分。一个典型的details路由可能同时携带多个查询参数例如/details?foof1barb1组件通过Route.useSearch()读取这些参数const search Route.useSearch()如果每次导航哪怕只改了一个参数都让组件拿到一个全新的 search 对象、重新渲染整棵子树应用会在高频交互下产生大量无意义的渲染开销。TanStack Router 的解决方案分为两层structural sharing在两次状态之间尽可能复用未变化的引用fine-grained selectors让组件只订阅它真正关心的状态子集。这两者可以独立使用也可以组合使用下面分别展开。二、Structural Sharing让未变化的数据保持引用稳定2.1 核心行为Structural sharing 的核心目标是在两次重渲染之间尽可能多地保留引用。这对存储在 URL 中的状态如 search 参数尤其有用。仍以details路由为例当从/details?foof1barb1导航到/details?foof1barb2仅bar变化时const search Route.useSearch()得益于 structural sharingsearch.foo的引用保持不变只有search.bar被替换。这带来的直接收益是以search.foo作为 props 或依赖的子组件经React.memo等机制不会重新渲染。2.2 源码级原理replaceEqualDeep这一行为由 utils.ts 中的replaceEqualDeep函数实现核心实现export function replaceEqualDeepT( prev: any, _next: T, _makeObj () ({}), _depth 0, ): T { if (isServer) { return _next } if (prev _next) { return prev } if (_depth 500) return _next // ... 逐 key 递归比较 }从源码可以提炼出几个关键实现事实浅层引用相等短路prev _next时直接返回prev这是最高频的命中路径逐 key 递归合并对普通对象与数组逐个 key 递归调用replaceEqualDeep只有「相等的子值」才从prev复用引用变化的子值取自next最后组装出一个新的外层对象copy整体相等则整体复用当所有子项都相等equalItems prevSize时直接返回prev本身连外层引用都保持不变深度限制递归深度超过 500 层时直接返回next防止极端嵌套数据导致栈溢出仅针对纯数据函数内部通过isPlainArray/isPlainObject判断只有普通数组与普通对象才会进入合并逻辑类实例等其他对象一律直接取next——这与文档中「仅支持 JSON 兼容数据」的约束完全一致服务端直通isServer为真时直接返回_next不做任何合并服务端只渲染一次无需保留引用。2.3 它是如何接入 Hook 的replaceEqualDeep本身不直接暴露给用户而是被 Hook 层包装为useStructuralSharing见 useMatch.tsx。该包装函数通过React.useRef保存上一次的选择结果在每次订阅回调中return (slice) { const selected opts?.select ? opts.select(slice) : slice if (opts?.structuralSharing ?? router.options.defaultStructuralSharing) { return (previousResult.current replaceEqualDeep( previousResult.current, selected, )) } return selected }即当structuralSharing开启时选择器每次返回的新值都会先与上一次结果做replaceEqualDeep合并从而把未变化的部分「还原」为旧引用。useSearch会把这个选择器委托给useMatch见 useSearch.tsx而useRouterState则通过useSelectoruseStructuralSharing订阅路由状态仓库见 useRouterState.tsx。三、Fine-Grained Selectors只订阅你关心的状态3.1 部分订阅useRouterState、useSearch等 Hook 允许你通过select属性做部分订阅只有选中值变化时组件才重渲染其余状态变化被忽略。// 当 bar 变化时本组件不会重渲染 const foo Route.useSearch({ select: ({ foo }) foo })这里的select是纯函数投影它接收完整的 search 对象或 router state返回你需要的子集。useSelector底层会做浅比较Object.is只有选择结果变化才会触发重渲染从而实现「按需订阅」。3.2 陷阱select 返回新对象会导致每次重渲染select可以做任意计算并返回不同类型的值——包括对象。例如const result Route.useSearch({ select: (search) { return { foo: search.foo, hello: hello ${search.foo}, } }, })这段代码可以工作但有一个性能陷阱每次调用select都会返回一个全新的对象字面量浅比较必然不相等因此组件会在每次相关状态变化时重新渲染——哪怕foo根本没有变。这正是 structural sharing 要解决的场景。四、组合使用给细粒度选择器开启 Structural Sharing要避免上述「select 每次返回新对象导致重渲染」的问题可以为细粒度选择器开启 structural sharing。开启后选择器返回的对象会先与上一次结果做replaceEqualDeep合并如果内部子值没有变化就会复用上一次的对象引用从而让浅比较通过、跳过重渲染。需要说明的是默认情况下 structural sharing 是关闭的以保持向后兼容。从 react-router/src/structuralSharing.ts 的类型定义可以看出当前默认值为false且源码中留有TODO in V2: default to true的注释——即 v2 中可能改为默认开启。开启方式有两种4.1 方式一在 Router 选项中全局开启const router createRouter({ routeTree, defaultStructuralSharing: true, })defaultStructuralSharing是 Router 构造选项之一其说明位于 router.ts。开启后所有未显式指定structuralSharing的 Hook 调用都会默认启用结构共享。4.2 方式二在每次 Hook 调用时单独开启const result Route.useSearch({ select: (search) { return { foo: search.foo, hello: hello ${search.foo}, } }, structuralSharing: true, })当 Router 选项中已全局开启defaultStructuralSharing: true时也可以在某次调用中显式关闭const result Route.useSearch({ select: (search) { return { date: new Date(), } }, structuralSharing: false, })两种方式可叠加使用Hook 级别的structuralSharing优先于 Router 级别的defaultStructuralSharing见 useMatch.tsx 中opts?.structuralSharing ?? router.options.defaultStructuralSharing的取值逻辑。五、约束与类型安全仅支持 JSON 兼容数据开启 structural sharing 有一个硬性约束[!IMPORTANT] Structural sharing 只适用于JSON 兼容数据。这意味着开启后select不能返回类实例等非 JSON 兼容的值。这一约束不仅是运行时行为replaceEqualDeep对非普通对象直接取next更被编码进了 TypeScript 类型系统。在 structuralSharing.ts 的类型定义中选择器返回值会经过ValidateSelected/ValidateJSON的约束const result Route.useSearch({ select: (search) { return { date: new Date(), } }, structuralSharing: true, })当structuralSharing: true时如果select的返回类型不满足 JSON 兼容约束例如包含Date实例TypeScript 会直接报编译错误将潜在 bug 拦截在编译期而非运行期。而如果 structural sharing 已在 Router 选项中全局开启你又确实需要返回非 JSON 值则可通过structuralSharing: false显式关闭单次调用的结构共享从而解除类型约束这正是上面「方式二」中structuralSharing: false示例的典型用途。六、适用场景与实践建议综合原文档与源码实现以下是几个高价值的实践场景多参数的 search 页面页面同时依赖foo、bar、page等多个查询参数但局部 UI 只关心其中一两个用selectstructuralSharing让局部组件对无关参数的变化无感派生计算select内对 search 做字符串拼接、过滤、排序等派生计算并返回对象时务必开启 structural sharing否则每次变化都会全量重渲染列表类状态search 中携带筛选数组等结构导航时若数组内容未变结构共享可保持数组引用稳定配合React.memo的列表项不会重渲染需要注意的边界开启后不要从select返回类实例、Map/Set、带 Symbol 键或不可枚举属性的对象——它们不属于 JSON 兼容数据既无法享受引用复用也可能被类型检查拒绝。相关的 API 文档可在仓库中继续深入useSearchHook.md、useRouterStateHook.md、RouterOptionsType.md含defaultStructuralSharing属性说明类型层面的完整约束见 packages/react-router/src/structuralSharing.ts 与 packages/router-core/src/structuralSharing.ts运行时算法与对应测试分别在 packages/router-core/src/utils.ts 和 packages/router-core/tests/utils.test.ts 中。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表