Skip to content

feat(signals): UNSTABLE_MEMO_OUTPUT attribution warning - #3034

Merged
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:unstable-memo-output
Aug 22, 2026
Merged

feat(signals): UNSTABLE_MEMO_OUTPUT attribution warning#3034
ryansolid merged 2 commits into
solidjs:nextfrom
brenelz:unstable-memo-output

Conversation

@brenelz

@brenelz brenelz commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #3018 (the "spec now" item from the warnings discussion): the fan-out amplifier detector.

What it catches

A memo that commits a referentially-new but shallowly-equivalent plain object/array on N consecutive runs (default 4) has an equality gate that never closes — every subscriber re-runs on every upstream change. A wide memo with fresh object output is the single worst fan-out machine in real apps, and until now was only findable by profiling.

[UNSTABLE_MEMO_OUTPUT] memo "view" produced a new-but-equivalent object on 4
consecutive runs — its equality gate never closes, so every subscriber re-runs
on every upstream change. Return stable references or pass an `equals` option.

Implementation — engine-side only, no new core hook sites

  • recomputeStart snapshots the node's committed value into the existing per-run frame (mirroring recompute's own pending-aware prev resolution, since an earlier run in the same flush may still be holding in _pendingValue).
  • The compare runs at recomputeEnd, not derivedChanged: that hook fires before the commit branches, when the new value is still a local inside recompute. By recomputeEnd it is observable via _pendingValue/_value, and the phase facts are in hand.
  • Plain objects and arrays only. Promises, Dates, Maps, class instances have no representative own enumerable keys — a fresh Promise per run is a genuinely new value, not instability. Pinned by a test.
  • Phase-aware: overlay (optimistic/transition) runs are excluded — a lane re-derive legitimately produces fresh equivalents while it settles.
  • Key-capped at 64 so hot paths stay cheap; once per streak; a genuinely different value resets the streak (so a later regression warns again).
  • Opt-out via enable({ unstableMemos: false }), threshold configurable.

One robustness fix surfaced by the tests: an explicit undefined in enable() options clobbers defaults through the spread, so the check treats any non-number as disabled rather than warning on run 1.

Tests

5 new in tests/attribution-unstable-memo.test.ts: warns once at threshold (runs: 4) for fresh-equivalent objects; arrays; streak resets on genuine change; silence for stable refs / primitives / Dates; opt-out. solid-signals suite green (1323).

🤖 Generated with Claude Code

The fan-out amplifier detector from the solidjs#3018 follow-up discussion: a
memo that commits a referentially-new but shallowly-equivalent plain
object/array on N consecutive runs (default 4) has an equality gate
that never closes - every subscriber re-runs on every upstream change,
a signature previously findable only by profiling.

Engine-side only, per review: no new core hook sites. recomputeStart
snapshots the node's committed value (mirroring recompute's own
pending-aware prev resolution); recomputeEnd shallow-compares on
plain, non-overlay changed memo runs. Plain objects and arrays only -
promises, dates, class instances have no representative own keys and
would false-positive. Key-capped at 64 so hot paths stay cheap.
Warns once per streak; a genuinely different value resets it.

Opt-out via enable({ unstableMemos: false }); threshold configurable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c383795

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@solidjs/signals Patch
test-integration Patch
solid-js Patch
babel-preset-solid Patch
@solidjs/web Patch
@solidjs/html Patch
@solidjs/h Patch
@solidjs/universal Patch
@solidjs/element Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryansolid
ryansolid merged commit 3028c36 into solidjs:next Aug 22, 2026
3 of 6 checks passed
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