LONG_HOLD
Acknowledged hold whose tail (last input → commit) ran 500ms+/1000ms+ (attribution enabled).
What you saw
- Kind:
responsiveness - Severity:
info(advisory, delivered on the diagnostics channel and never printed) orwarn(code that works but is structurally wrong or expensive)
The console report opens with [LONG_HOLD] 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: "LONG_HOLD", ownerPath, and loc.
Diagnostics explains each line.
Why it fires
The hold WAS acknowledged — an isPending() spinner, a latest() header, an optimistic value — and still ran on: the time from the user's last input to the commit (data.tailMs; holdMs is the whole wait when input kept arriving) reached longHolds.infoMs (default 500ms), warn from longHolds.warnMs (default 1000ms, where the user loses the thread).
A hold is the stale-while-revalidate tool: right when the old screen stays useful for the wait (tab switch, sort toggle, a fast page turn).
At this length the old screen has stopped being useful and started being a lie; "loading" glued over it reads as broken.
The runtime did the correct thing; the user saw nothing while it did. These fire only while attribution is enabled.
How to fix it
Show a fallback instead:
- Put the reader behind a
Loadingboundary keyed on what changed:<Loading on={page()} fallback={<Skeleton />}>. Withon, the write commits at once and the fallback shows where the data lands. Without it a boundary that has already revealed keeps the old content — that IS the hold, so wrapping alone changes nothing. - A boundary that has not revealed yet (a fresh route segment, a keyed subtree) takes the wait the same way;
onis the switch for one that has. - If the data itself is the problem, preload it (
markFlightthe kickoff so it is not misread as a waterfall) or cache it so the tail never gets there.
Do NOT remove the acknowledgement to "fix" this, and do not move the write off the async path.
The measure is the tail, not the lifetime, so a hold that keeps taking input (typing) is judged by each wait, not by the sum.
feedback().sources[].long/longMs counts these at the table level, acknowledged or not.
In tests
captureArtifact from @solidjs/diagnostics runs a scenario with the diagnostics channel open and returns the events it saw.
expectNoDiagnostics(artifact) fails while LONG_HOLD is among them, with the events as evidence; after the fix, the code is gone from artifact.diagnostics.
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.