perf: dedupe signal resolution in core signal hooks#145
Open
mbret wants to merge 1 commit into
Open
Conversation
useSignalReference read the SignalContext twice per render: once directly and again inside useMakeOrRetrieveSignal. Collapse to a single useContext read by inlining the virtual-signal lookup. useSignal now passes the already-resolved signal to useSignalValue and useSetSignal instead of the raw input, so the (virtual) signal is resolved through the context once per render instead of three times. Behavior is unchanged: getOrCreateSignal is idempotent, so the resolved instance is identical to what each child hook produced before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgDrYRTXLbRPYELZvShgk3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Target
Subsystem: state / react signal hooks (
useSignalReference,useSignal) — the core state-reading API used byuseSignal,useSignalValue,useSignalStateanduseSetSignal.User-visible symptom: redundant per-render work in the hooks that (typically many) components mount to read signals. No functional bug; pure overhead on the hottest hook family.
Changes
Each change is invisible in behavior (identical output/identity) and only removes redundant work.
useSignalReferenceread the context twice → once.Mechanism: it called
useSignalContext()directly and again insideuseMakeOrRetrieveSignal, so every call performed twouseContextreads of the same context. Inlining the virtual-signal lookup collapses this to a singleuseContext.Scale multiplier: one redundant context read per
useSignalReferencecall × every render of every signal-consuming component.useSignalresolves the signal once per render instead of three times.Mechanism:
useSignalcalleduseSignalReference(signal)and thenuseSignalValue(signal)/useSetSignal(signal), each of which re-resolves the signal internally. Passing the already-resolvedfinalSignaldown means the (virtual → concrete) resolution — a context read plus aMaplookup viagetOrCreateSignal— happens once at the top instead of three times.Scale multiplier: for a
VirtualSignal, this removes 2 redundantgetOrCreateSignalmap lookups peruseSignalrender; the resolved instance is unchanged becausegetOrCreateSignalis idempotent.Impact
Constant-factor reduction of per-render work in the hottest hook family — fewer
useContextreads andMaplookups per render, multiplied by (components using signal hooks × render frequency). This is React-runtime/hook overhead rather than a pure function, so it is not meaningfully micro-benchmarkable in isolation; the justification is the removed redundancy (duplicate context read, triple signal resolution) proven identical by the existing suite.Verification
Ran the full CI gate set on a clean
maincheckout (baseline) and after the change:npm run check(biome) — clean, no changesnpm run build(tsc + vite) — succeedsnpm run test:ci— 129 passed / 129 in both runsNo new failures introduced; behavior identical.
useMakeOrRetrieveSignalis left in place as it remains part of the public API surface.Backlog (performance opportunities found but not taken)
src/lib/utils/filterObjectByKey.tsuses an accumulating spread insidereduce(O(n²), already flagged by biome), but it is unused across the library — a dead-code candidate rather than a perf target.arrayEqualwas evaluated for a.every(callback)→ indexedforrewrite; benchmarking at n = 100/1k/10k showed thefor-loop is actually slower (0.72–0.92×) under V8, so it was intentionally left alone.useObservecreates its ownObservableStore(distinctUntilChanged+tap+share+ an eager subscription). N components observing the same source produce N independent pipelines/subscriptions — inherent to the per-hook design; a shared-store cache would be a larger, behavior-sensitive change.Generated by Claude Code