拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Preact Query 渲染优化权威指南:structural sharing、属性追踪与 select 的底层原理与实战

Preact Query 渲染优化权威指南structural sharing、属性追踪与 select 的底层原理与实战【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/queryTanStack Query 官方在tanstack/preact-query中对查询结果的分发做了一系列自动渲染优化只有组件真正用到的结果属性发生变化时才会触发重新渲染同时通过结构性共享structural sharing在两次渲染之间尽量复用旧数据的对象引用配合select做细粒度订阅让中等规模列表页、高频轮询页面都能在保持代码直观的前提下显著减少无效渲染。本文以仓库内 docs/framework/preact/guides/render-optimizations.md 指南为骨架逐层拆解这三类优化手段的实现机制、可调参数与坑位并结合packages/preact-query与packages/query-core的源码给出可验证的依据。读完你将能回答Preact Query 什么时候会重新渲染、为什么、以及如何按需关掉或自定义这套机制。说明本指南在仓库中是框架共享文档——Preact 版文件的 frontmatter 声明了ref: docs/framework/react/guides/render-optimizations.md并约定将react-query替换为preact-query、React替换为Preact后生成。因此它讨论的是tanstack/preact-query这一适配层的真实行为而其机制核心observer、缓存、变更通知位于跨框架共享的tanstack/query-core源码路径见下文标注。为什么 Preact Query 要主动做渲染优化在典型的服务端状态场景里一个 query 的状态对象包含的字段远比单个组件关心的要多data、status、error、isPending、isFetching、isStale、isRefetching、fetchStatus……其中isFetching、isStale这类标志在后台重取、窗口聚焦刷新时变化非常频繁而多数组件其实根本不读取它们。如果任何字段变化都导致订阅方重渲染应用会在高频刷新时白白浪费大量渲染帧。Preact Query 的解决思路分三层、各司其职structural sharing——稳定data的对象引用让数据没变这件事可以被廉价地比较出来tracked properties属性追踪——只在你真正读取某个属性时才把该属性登记为变更需要通知的对象select——把组件订阅的数据面从整份缓存收窄到派生结果。三者叠加最终让组件仅在其实际观察到的数据片段发生变化时才重渲染。structural sharing尽可能保留对象引用机制与问题背景通过网络请求拿到的数据通常要经过JSON.parse/ 反序列化得到的是一个全新的对象引用——哪怕服务端返回的内容与上一次逐字节相同。如果框架每次都把这份新对象塞给组件data的引用会无条件变化随之而来的就是无谓重渲染、子组件 memo 失效、useEffect/useMemo依赖连锁触发。structural sharing 要解决的问题正是这一点在更新缓存数据时逐层比较新旧值能复用旧引用的部分就复用旧引用只有真正变化的子节点才替换为新值。这一算法在核心包中对应replaceEqualDeep实现位于 packages/query-core/src/utils.ts其基准行为在 packages/query-core/src/tests/utils.test.tsx 中有大量用例覆盖例如对原始值、Date、对象、数组、undefined替换语义的断言。默认行为与启用前提该优化默认开启无需任何配置。它的生效有一个硬性前提只有当queryFn返回的是JSON 兼容数据普通对象/数组/原始值时structural sharing 才有意义。对Map、Set、类实例等非纯数据对象参考级别的整体替换依然是正确行为。从源码结构可以确认该配置同时作用于 query 缓存写入setQueryData/queryFn成功结果的缓存更新与select派生值的处理packages/query-core/src/tests/queryClient.test.tsx 验证了structuralSharing: false时不比较、直接写入新数据以及传入自定义函数时按自定义逻辑执行packages/query-core/src/tests/queryObserver.test.tsx 验证了在默认开启时select派生值同样会走深度相等比较而structuralSharing: false或存在placeholderData时其行为会相应调整。如何关闭或自定义按官方指南可以全局关闭、单 query 关闭或传入自定义 sharing 函数import { QueryClient } from tanstack/preact-query // 全局关闭 const queryClient new QueryClient({ defaultOptions: { queries: { structuralSharing: false, }, }, }) // 单个 query 关闭 useQuery({ queryKey: [todos], queryFn: fetchTodos, structuralSharing: false, }) // 自定义共享策略传入一个 (oldData, newData) sharedData 的函数 useQuery({ queryKey: [todos], queryFn: fetchTodos, structuralSharing: (oldData, newData) /* 自定义深度共享逻辑 */ newData, })什么时候值得关掉当你的数据结构极其扁平、几乎每次都会整体变化或数据结构复杂到深度遍历反而成为性能瓶颈时关闭它可以省下比较的开销反之默认开启在绝大多数场景是净收益。与referential identity引用同一性的关系structural sharing 保证了data的稳定但请注意结果的顶层容器并不稳定useQuery、useInfiniteQuery、useMutation返回的顶层对象以及useQueries返回的数组每次渲染都是新引用不具备 referential stability。稳定的是这些 hook 返回的data字段本身。在 packages/preact-query/src/useBaseQuery.ts 可以看到每次渲染都会先通过observer.getOptimisticResult(defaultedOptions)现算结果再经由useSyncExternalStore订阅。因此如果你把data整体放进某个 memo 依赖它能保持稳定但若把整个result对象或useQueries数组作为依赖则每次都会变化。实践中常见的写法是单独解构出data后再交给 memo/子组件让引用稳定性红利真正生效。tracked properties用 Proxy 只追踪你用到的属性实现原理从tanstack/preact-query的角度看这一特性是透明的它不引入任何额外的渲染循环而是在 hook 底部做一次属性使用登记。核心逻辑见 packages/preact-query/src/useBaseQuery.ts// Handle result property usage tracking return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) : result即当未手动设置notifyOnChangeProps时返回给组件的是trackResult包了一层 Proxy 的结果对象。Proxy 的gettrap 会把被访问的每个属性名加入 observer 的追踪集合packages/query-core/src/queryObserver.tstrackResult返回new Proxy(result, { get: (target, key) { this.trackProp(key); return Reflect.get(target, key) } })同一文件第 65 行的#trackedProps new Setkeyof QueryObserverResult()记录本次渲染实际访问过哪些属性。随后在观测者决定是否通知监听者时queryObserver.ts会检查被追踪属性集合与本次变化的属性集合是否有交集只要组件用到的属性没变就不会触发重渲染。于是isFetching、isStale这类频繁翻转的属性若组件从不读取就永远不会把组件吵醒。两种触发登记的写法对比Proxy 的gettrap 只有在访问属性时才会被调用——无论是通过解构还是直接result.data。所以下面两种写法都能享受追踪优化import { useQuery } from tanstack/preact-query // 写法一解构推荐读取即登记 const { data, isPending } useQuery({ queryKey: [todos], queryFn: fetchTodos }) // 写法二直接访问 const result useQuery({ queryKey: [todos], queryFn: fetchTodos }) // result.data / result.isPending 的访问会被登记但有一个高危坑位rest 解构object rest destructuring会一次性取走剩余的所有属性从而禁用追踪优化。// ⚠️ 不要这样写rest 解构会把整个结果读一遍追踪优化失效 const { data, ...rest } useQuery({ queryKey: [todos], queryFn: fetchTodos })因为 rest 模式会遍历并读取目标对象上所有未显式命名的自有属性导致所有属性都被登记为被使用Proxy 追踪形同虚设。仓库为此提供了一条专门的 ESLint 规则做静态拦截详见 docs/eslint/no-rest-destructuring.md实战中建议在eslint.config.js中启用该规则让 CI 替团队把关。关闭或自定义追踪追踪并非一刀切的黑魔法官方指南给出了两层旋钮// 单 query 关闭追踪等价于所有属性变更都通知 useQuery({ queryKey: [todos], queryFn: fetchTodos, notifyOnChangeProps: all, }) // 手动声明关心的属性追踪范围即白名单 useQuery({ queryKey: [todos], queryFn: fetchTodos, notifyOnChangeProps: [data, isPending], })同样可以在QueryClient的defaultOptions.queries里全局设置all或属性名数组。从 useBaseQuery.ts 的分支可以看到只要设置了notifyOnChangePropshook 就跳过trackResult直接把结果原样返回——追踪逻辑完全按你给定的模式执行。需要说明的是手动白名单是静态的、需要你手动维护而 Proxy 追踪是动态的、随组件代码自动收敛因此日常开发优先依赖默认追踪只有当你想精确锁定某几个属性、或遇到需要全量变更都通知的边界场景时才去动notifyOnChangeProps。select把订阅收窄到派生结果用法与收益当组件只关心缓存数据的某个切片时可以用select声明派生关系。缓存里始终保存完整数据而组件拿到的data是select的输出。指南中给出了一个高度可复用的封装模式import { useQuery } from tanstack/preact-query // 对外暴露可注入 select的 query hook export const useTodos (select) { return useQuery({ queryKey: [todos], queryFn: fetchTodos, select, }) } // 组件 A只需要 todo 数量 export const useTodoCount () { return useTodos((data) data.length) }使用useTodoCount的组件只在 todos 的 length 变化时才重渲染——比如仅仅某条 todo 的 name 改了这个组件纹丝不动。这正是列表页计数徽标与列表主体解耦、避免互相拖累的经典姿势。select 不是做错误处理的地方指南对select的职责边界做了非常明确的提醒值得照抄进团队规范select运行在已成功缓存的数据之上它不是抛错的合适位置。错误的唯一事实来源source of truth是queryFn。如果一个select函数返回错误结果会是data undefined且isSuccess true——一个成功但没有数据的诡异状态。建议如果你希望数据不正确就让整个 query 失败请在queryFn内做校验并抛错如果错误与缓存无关例如纯粹的渲染期错误请在 query hook 之外处理。记忆化memoization为什么内联 select 会在每次渲染都执行select函数并不是每次都重新执行。它只会在以下两个条件之一满足时重新运行select函数本身发生了引用变化缓存data发生了变化。由此引出一个关键结论内联的select函数每次渲染都是新引用所以它会在每次渲染都执行一遍——这在数据量大时同样是一种浪费。指南给出两种修正方式import { useCallback } from preact/hooks // 方式一用 useCallback 包一层空依赖保证引用稳定 export const useTodoCount () { return useTodos(useCallback((data) data.length, [])) }// 方式二抽成模块级稳定函数引用无依赖时的最优解 const selectTodoCount (data) data.length export const useTodoCount () { return useTodos(selectTodoCount) }在源码层面与select配合的 structural sharing 行为同样有测试背书默认开启 structural sharing 时select的派生值也会经replaceEqualDeep做相等比较queryObserver.test.tsx这意味着即便select每次执行都返回结构等价的新对象只要内容相等下游仍能拿到稳定引用——所以用 useCallback 稳定 select与核心层的结构相等比较是两层互补的优化。组合起来一个高吞吐场景的完整范例把三项机制叠加可以写出既直观又高效的组件。下面以轮询刷新 计数徽标 列表为例import { useCallback } from preact/hooks import { useQuery } from tanstack/preact-query const fetchTodos () fetch(/api/todos).then((r) r.json()) // 列表主体依赖默认的 Proxy 属性追踪 // 后台刷新导致的 isFetching 变化不会让整个列表重渲染 function TodoList() { const { data, isPending } useQuery({ queryKey: [todos], queryFn: fetchTodos, refetchInterval: 30_000, // 30s 轮询 }) if (isPending) return 加载中... return ( ul {data.map((todo) ( li key{todo.id}{todo.name}/li ))} /ul ) } // 计数徽标select 稳定引用只关心 length const selectCount (data) data.length function TodoCount() { const { data: count, isPending } useQuery({ queryKey: [todos], queryFn: fetchTodos, select: selectCount, // 模块级稳定函数引用 }) if (isPending) return null return span共 {count} 项/span }在这个例子里structural sharing保证轮询返回的、内容未变的数据不会替换整个缓存引用tracked properties保证TodoList只在它真正读取的data/isPending变化时重渲染isFetching的每秒翻转与之无关select 稳定函数引用保证TodoCount只对length变化敏感某条 todo 的 name 修改不会打扰它。与 query-core 的分工为什么这套机制对每个框架都一致值得强调的一点上面拆解的#trackedProps、trackResult、replaceEqualDeep全部位于 packages/query-core/src 这个跨框架共享核心中queryObserver.ts、utils.ts而 packages/preact-query 作为薄适配层只负责把 observer 接到 Preact 的useSyncExternalStore见 utils.ts与生命周期上。这也解释了为什么你从tanstack/preact-query得到的优化行为和 React/Vue/Solid/Svelte 各适配层在语义上完全同源——区别仅在于各框架的渲染与订阅原语。所有useQuery变体含useInfiniteQuery、useQueries、useMutation最终都汇聚到 useBaseQuery.ts 的同一套返回/追踪路径上例如 useQuery.ts 中useQuery就是useBaseQuery(options, QueryObserver, queryClient)的一行转调。进一步阅读官方渲染优化细则的可复现主文档docs/framework/react/guides/render-optimizations.md本文档的生成来源追踪优化的静态防线ESLint 规则docs/eslint/no-rest-destructuring.mdtrackResult/trackProp/变更通知判定源码packages/query-core/src/queryObserver.tsPreact 适配层追踪开关分支packages/preact-query/src/useBaseQuery.tsstructural sharing 基准算法与测试packages/query-core/src/utils.ts、packages/query-core/src/tests/utils.test.tsx应用级选项全局默认值入口new QueryClient({ defaultOptions: { queries: { ... } } })实践结论可以浓缩为一句默认开着就好别动notifyOnChangeProps别写内联select绝不用 rest 解构拆查询结果——当渲染性能仍不达标时优先怀疑组件自身的 memo 边界而不是这套已经替你兜底的机制。【免费下载链接】query Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门