WASTED_RECOMPUTE
80%+ of a scope's 5+ runs in a second produced an unchanged value for 2ms+ of compute — inputs change, result does not (attribution enabled).
What you saw
- Kind:
perf - Severity:
warn(code that works but is structurally wrong or expensive)
The console report opens with [WASTED_RECOMPUTE] and the message, then an in line naming the owners that enclose the subject, root first, and an at line with the source location when the code was compiled with one.
In a captured artifact the same finding is an event with code: "WASTED_RECOMPUTE", ownerPath, and loc.
Diagnostics explains each line.
Why it fires
The mirror of UNSTABLE_MEMO_OUTPUT: this scope's equality gate closes almost every time.
data.wasted of data.runs runs in the window produced the same value as before — the scope re-ran because an input changed, did its work, compared equal, and told nobody.
The compute (data.wastedMs) bought nothing.
data.causes names the input that keeps triggering it.
These fire only while attribution is enabled and describe cost, not incorrect behavior. The numbers in the message are measurements, not guesses.
How to fix it
Fix upstream, where the change originates:
- Read a narrower slice:
user().namere-runs on everyuserwrite; ifuseris a store,user.namere-runs only when the name changes. For a signal holding an object, derive the field first —const name = createMemo(() => user().name)— and read the memo. - Put an equality boundary on the source: an
equalsoption that compares the part that matters, so writes that do not change it never notify. - If the scope is expensive and the input legitimately churns, split it: a cheap memo that extracts what it needs, feeding the expensive one.
Do NOT add an equals to the wasted scope itself — its gate already closes; the cost is the run before the gate.
The run is the problem, so stop the notification that starts it.
Held and overlay runs are never counted here; see costs().scopes[].wastedMs for the total per scope.
In tests
captureArtifact from @solidjs/diagnostics runs a scenario with the diagnostics channel open and returns the events it saw.
expectNoDiagnostics(artifact) fails while WASTED_RECOMPUTE is among them, with the events as evidence; after the fix, the code is gone from artifact.diagnostics.
expectRerunBudget(artifact, max, { scope }) bounds how often a named scope may re-run, and expectNoWaste(artifact) fails on re-runs whose result was unchanged.
Both read the attribution records, which captureArtifact collects unless attribution: false is passed.
Check for regressions in tests shows the loop and the Vitest matcher form.
Learn more
- Diagnostics: every code, with its kind, severity, and trigger.
- Debugging reactivity: how to read a report and what each group of codes points at.
- Attribution: the engine whose records explain a re-run, a hold, or a waterfall.