Files
Neil 4120501979 perf(store): detect Zustand rerender churn the current audit cannot see (#19059)
* perf(store): detect Zustand rerender churn the current audit cannot see

The app-store-performance audit only understood inline selectors passed to a
hook imported literally as `useAppStore`, so three shapes went unlinted:

- a selector referenced by name (`useAppStore(selectRows)`), including one
  hoisted below its call site — resolved now via a Program:exit pass
- the sibling store hooks (`usePluginPanelsStore` and friends), matched by the
  use<Name>Store convention on local imports; React's `useSyncExternalStore`
  matches that shape and is excluded
- a fresh reference nested inside a `useShallow` projection, which is the worst
  case of the three: the comparator runs on every write and can never match, so
  the memo silently buys nothing

`no-nested-fresh-under-shallow` covers the last one. `src` is clean against all
four rules today, so this is a ratchet rather than a cleanup.

The write side stays undecidable statically — whether a `set()` reallocated for
nothing depends on the payload — so it gets a runtime probe instead.
withStoreIdentityChurnProbe counts writes that replace a field's reference while
its value stays equal, and can name the calling site. Cost when disarmed is one
boolean load per write, matching react-commit-cascade-write-probe.

* perf(store): scope the churn probe's scan to the write's own keys

recordWrite iterated Object.keys of the full post-write state, so the armed cost
scaled with the store's top-level field count (hundreds) rather than the size of
the write. `set(partial)` merges, so no field outside the partial can have changed.

The wrapper now resolves a functional updater itself and iterates the resolved
partial's keys. Same function, same argument, called once — there is a test
pinning that, since calling it twice would double any work a slice does inside
its own updater. A replace write drops absent fields, so that path still scans
every field.

Disarmed cost is unchanged: one boolean load.

* perf(store): follow a selector one hop into its helper

Review feedback: both the lint rule and the manual sweep it was checked against
only looked at the inline selector body, so neither could see a fresh allocation
made inside a helper the selector calls — and delegating to a module-scope helper
is the idiomatic shape here. Two methods sharing a blind spot is not corroboration.

The two fresh-reference rules now resolve a single hop into a module-scope helper.
The predicate used across that hop is deliberately stricter than the inline one:
it requires EVERY returned expression to allocate unconditionally, so the common
`cache.get(k) ?? buildFresh(state)` identity-caching shape is not flagged. An
unresolvable helper is left alone rather than guessed at.

Still zero hits across 20,330 files, so this stays a ratchet.

* perf(store): keep the churn probe off the shipped write path

Review hardening for the churn probe and the widened lint rules.

Probe: it no longer resolves a functional updater itself. Zustand keeps sole
ownership of when and with what argument an updater runs, so the middleware
cannot double-invoke it or hand it a stale state. Object partials still scope
the scan to the write's own keys; updater and replace writes fall back to the
full field list, which costs one Object.is per untouched field and nothing
more, since the deep compare only runs on replaced references.

store/index.ts installs the probe only when import.meta.env.DEV or
e2eConfig.exposeStore is set, the same gate as __store exposure. Nothing in the
app arms it, so a shipped build was paying a wrapper frame per write for a
diagnostic it could never read. The cascade probe stays unconditional because
crash telemetry arms it in the field.

Site capture now skips any *-probe.ts frame; under the real composition the
first non-node_modules frame was the cascade probe's wrapper, so every churn
was attributed to react-commit-cascade-write-probe.ts:32 instead of the caller.

Plugin: named-selector recording is restricted to module scope. A
component-local `const selectRows = ...` used to overwrite the entry for a
same-named imported selector and flag an unrelated useAppStore(selectRows).
The any-branch and every-branch allocation predicates are one function with a
flag, the Object.* static list is a Set, and import recording is a single pass.

Tests: updater called once with live state, identical-state writes ignored,
disarmed path forwards exact arguments without calling get(), full composition
with the cascade probe (no drop, no double, correct site), and the
module-scope shadowing case for the plugin.
2026-09-06 15:12:19 -07:00
..