Repository navigation
Description
Summary
useClientQueries(schema) rebuilds the full hooks object on every call: for every model in the schema it allocates a fresh set of ~24 hook closures, and useModelQueries locates each model with a linear scan of schema.models that calls toLowerCase() on both sides of every comparison. Nothing is cached, so the cost is paid on every render of every component that calls it. The helper calls no React hooks itself, so there is nothing that prevents building the object once per schema.
The documented usage pattern is useClientQueries(schema).model.useFindMany(...) inside components, which means any app following the docs pays the whole rebuild to reach one model.
On a 149-model schema with ~1,000 call sites, a 74.5 s Chrome performance trace of one page attributed 43.2 s of self time to useClientQueries alone (63 % of each of the seven longest main-thread freezes). Worst INP was 5.4 s. The allocation churn also showed up as 311 scavenge GCs / 1.2 s.
Where
packages/clients/tanstack-query/src/react.ts (identical in the 3.10.0 and 3.9.6 builds, and unchanged on main today):
export function useClientQueries(schema, options) {
return Object.keys(schema.models).reduce((acc, model) => {
acc[lowerCaseFirst(model)] = useModelQueries(schema, model, options);
return acc;
}, {});
// ... $procs, $transaction
}
export function useModelQueries(schema, model, rootOptions) {
// O(models) scan per model → O(models²) per useClientQueries call
const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase());
...
return { useFindUnique, useFindMany, useCreate, /* ~24 closures */ };
}Per call on the 149-model schema this is 11,175 name comparisons, 22,350 throwaway lowercased strings, and 3,576 closure allocations. vue.ts and svelte/index.svelte.ts have the same shape.
Measured
Benchmark against the real generated schema, 2,000 iterations, Node 24 (same figure whether the hooks object is consumed or not):
| ms / call | |
|---|---|
useClientQueries(schema) as published (3.10.0) |
0.555 |
| same, memoised per schema (patch below) | 0.00009 |
The cost is quadratic in model count, so it is invisible on small schemas and dominant on large ones.
Proposed fix
Two independent changes:
-
Memoise per schema object. Cache the result in a
WeakMap<SchemaDef, ClientHooks>on the no-options path (theoptionsform would need to be part of the key, or left uncached). The returned closures only close overschemaandoptions, so a shared object is observationally identical except thatuseClientQueries(schema)becomes referentially stable across renders, which is also friendlier to dependency arrays. Rules of hooks are not affected: the real hook calls still happen inside the returned closures at the same points in render as today. -
Index models once in
useModelQueriesinstead of rescanningObject.values(schema.models)withtoLowerCase()per lookup. That turns the single remaining build from quadratic to linear and helps anyone callinguseModelQueriesdirectly.
This is the patch we are running in production via pnpm patch against dist/react.js; it implements (1) only:
+const __clientQueriesCache = new WeakMap();
export function useClientQueries(schema, options) {
+ if (!options) {
+ const __cached = __clientQueriesCache.get(schema);
+ if (__cached)
+ return __cached;
+ }
const result = Object.keys(schema.models).reduce((acc, model) => {
...
result.$transaction = {
useSequential: (hookOptions) => useInternalTransactionMutation(schema, { ...options, ...hookOptions }),
};
+ if (!options)
+ __clientQueriesCache.set(schema, result);
return result;
}Verified against the full schema: same 150 keys, 24 hooks per model, $transaction.useSequential intact, every value still a function, and the app's full unit suite (1,342 files) passes unchanged with the patch applied.
Same family as #2773 and #2715 (uncached full-schema work on a hot path).
Environment
- ZenStack version: 3.10.0 (
@zenstackhq/tanstack-query,@zenstackhq/orm);@tanstack/react-query5.105.0; React 19.3.1; Next.js 16 - Database type: PostgreSQL
- Node.js version: 24
- Package manager: pnpm 12
Activity
perf(deps): memoise useClientQueries per schema via pnpm patch
chore(release): 1.2.0-beta.31 [skip ci]