Diagnostics

GRAPH_GROWTH

The live graph and what outlived each navigation climbed at a route's settle on 3 visits to 1.25× — something each visit leaves behind (attribution enabled).


What you saw

  • Kind: perf
  • Severity: warn (code that works but is structurally wrong or expensive)

The console report opens with [GRAPH_GROWTH] 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: "GRAPH_GROWTH", ownerPath, and loc. Diagnostics explains each line.


Why it fires

The live graph, counted when each navigation settles, got bigger on every one of the last data.history.length visits to data.route, and so did the part of it that outlived each of those navigations (data.survived, beside data.history: what was alive as this or an earlier navigation began and still is) — something each visit creates is never disposed. A page that is merely bigger on one visit than the last does not report: its content is new at each arrival, so it is not in data.survived. Long-lived module state (a global store, the HMR registry signal of each component in dev) counts the same at every settle once a navigation has met it, so it never climbs. Navigations that only change a route's params or query continue the visit and are not compared. This is the leak a heap snapshot finds; the engine finds it by measuring the reactive graph as each navigation opens and at its settle. data.grew says which measure climbed, and that names the leak: owners is an undisposed root or a Portal mounted per visit; computations with owners flat is an effect or memo created with no owner, kept alive only by the sources it reads; edges alone is a subscription per visit to something long-lived. The count is the whole app's, so data.routes names every route seen while it climbed; the culprit is on one of them.

Performance pathologies (from the attribution engine)

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

Look for, in order:

  • A createRoot() in an effect, an event handler, or a module-level helper whose disposer is dropped. Return it from onSettled (the component's setup-and-teardown), or call it in onCleanup.
  • A subscription registered outside the owner that should tear it down — an addEventListener, an observer, a store subscription — made in a plain function rather than under the component. Move it under the owner and pair it with onCleanup.
  • A Portal, a panel or a modal mounted on each visit into a long-lived root, never removed when the route leaves.
  • A cache (entities kept by key, each under its own root) that only ever adds. Its entries outlive every navigation, so to the walk it is the same growth as a leak, and it is: memory that never comes back. Bound it or evict entries no visit still reads; an evicting cache stays quiet.

Do NOT reach for dispose() on the app root or a periodic sweep; the fix is ownership — create the thing under the owner whose lifetime it should share. graphSize() from solid-js/attribution gives the count on demand; OBSERVE.records.subscribe("graph", …) gives it at every navigation's settle.

The initial route declaration (initial: true) emits a graph record but is excluded from growth comparisons because it settles before route content mounts. Later visits to that route count as usual.


In tests

captureArtifact from @solidjs/diagnostics runs a scenario with the diagnostics channel open and returns the events it saw. expectNoDiagnostics(artifact) fails while GRAPH_GROWTH 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.
Last updated: 10/15/26, 10:33 PMEdit this pageReport an issue with this page