Skip to content

perf: dedupe signal resolution in core signal hooks#145

Open
mbret wants to merge 1 commit into
mainfrom
chore/perf-2026-07-20-signal-hook-resolution
Open

perf: dedupe signal resolution in core signal hooks#145
mbret wants to merge 1 commit into
mainfrom
chore/perf-2026-07-20-signal-hook-resolution

Conversation

@mbret

@mbret mbret commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Target

Subsystem: state / react signal hooks (useSignalReference, useSignal) — the core state-reading API used by useSignal, useSignalValue, useSignalState and useSetSignal.

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.

  1. useSignalReference read the context twice → once.
    Mechanism: it called useSignalContext() directly and again inside useMakeOrRetrieveSignal, so every call performed two useContext reads of the same context. Inlining the virtual-signal lookup collapses this to a single useContext.
    Scale multiplier: one redundant context read per useSignalReference call × every render of every signal-consuming component.

  2. useSignal resolves the signal once per render instead of three times.
    Mechanism: useSignal called useSignalReference(signal) and then useSignalValue(signal) / useSetSignal(signal), each of which re-resolves the signal internally. Passing the already-resolved finalSignal down means the (virtual → concrete) resolution — a context read plus a Map lookup via getOrCreateSignal — happens once at the top instead of three times.
    Scale multiplier: for a VirtualSignal, this removes 2 redundant getOrCreateSignal map lookups per useSignal render; the resolved instance is unchanged because getOrCreateSignal is idempotent.

Impact

Constant-factor reduction of per-render work in the hottest hook family — fewer useContext reads and Map lookups 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 main checkout (baseline) and after the change:

  • npm run check (biome) — clean, no changes
  • npm run build (tsc + vite) — succeeds
  • npm run test:ci129 passed / 129 in both runs

No new failures introduced; behavior identical. useMakeOrRetrieveSignal is left in place as it remains part of the public API surface.

Backlog (performance opportunities found but not taken)

  • src/lib/utils/filterObjectByKey.ts uses an accumulating spread inside reduce (O(n²), already flagged by biome), but it is unused across the library — a dead-code candidate rather than a perf target.
  • arrayEqual was evaluated for a .every(callback) → indexed for rewrite; benchmarking at n = 100/1k/10k showed the for-loop is actually slower (0.72–0.92×) under V8, so it was intentionally left alone.
  • Each useObserve creates its own ObservableStore (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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants