Skip to content

fix: release lazy() resource and suspense contexts on dispose#2730

Open
MRJCrunch wants to merge 1 commit into
solidjs:mainfrom
MRJCrunch:fix/lazy-resource-leak
Open

fix: release lazy() resource and suspense contexts on dispose#2730
MRJCrunch wants to merge 1 commit into
solidjs:mainfrom
MRJCrunch:fix/lazy-resource-leak

Conversation

@MRJCrunch
Copy link
Copy Markdown

Summary

lazy() components leak the previous component tree across navigation when lazy() boundaries are nested.

Root cause

  • lazy() caches its createResource accessor in a module-scoped comp variable, which pins the resource's reactive graph for the lifetime of the app.
  • createResource never releases its Suspense contexts set (nor the pending transition promise) on disposal.

When lazy() boundaries are nested (e.g. a router where a lazy route renders an outlet that contains another lazy route), the inner resource's reactive nodes are parented under the outer, module-pinned graph. Because that graph is never released and the resource never clears its contexts on dispose, the disposed component subtree — and its detached DOM — stays reachable and is never garbage collected. Each navigation leaks another tree.

A single lazy() boundary does not leak; only nested/stacked lazy() does.

Fix

  • lazy() clears the module-scoped accessor (comp) in onCleanup. The import promise p stays cached, so unmount/remount does not refetch the chunk.
  • createResource clears its Suspense contexts (decrementing each) and removes its pending promise from the active Transition when its owner is disposed. Guarded by if (Owner) so resources created outside a reactive root behave exactly as before.

Verification

  • pnpm build + pnpm test: 469/469 tests pass, no new warnings.
  • Validated against a production app with deeply nested lazy routes via heap snapshots: the leaked tree (a large component instance + ~1500 detached DOM nodes per navigation) drops to 0 with the patch.

A changeset is included (solid-js patch).

Nested lazy() components leaked the previous tree across navigation.
lazy() cached its createResource accessor in a module-scoped variable,
pinning the resource's reactive graph for the app lifetime, and
createResource never released its suspense contexts on disposal. With
stacked lazy() boundaries that kept disposed component trees (and their
detached DOM) reachable, so they were never garbage collected.

lazy() now clears the module-scoped accessor on cleanup (the import
promise stays cached, so no refetch), and createResource clears its
suspense contexts and pending transition promise when its owner is
disposed.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 51d4779

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

This PR includes changesets to release 2 packages
Name Type
solid-js Patch
test-integration 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

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.

1 participant