diff --git a/.changeset/fix-lazy-resource-leak.md b/.changeset/fix-lazy-resource-leak.md new file mode 100644 index 000000000..8e128d07c --- /dev/null +++ b/.changeset/fix-lazy-resource-leak.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +Fix memory leak with nested `lazy()` components. `lazy()` cached its `createResource` accessor in a module-scoped variable and `createResource` never released its Suspense contexts on disposal, so a disposed component tree (and its detached DOM) stayed reachable across navigations when `lazy()` boundaries were nested. The module-pinned accessor is now released on cleanup, and the resource clears its Suspense contexts and pending promise on disposal. diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index d47844c01..f4f0309ab 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -633,6 +633,14 @@ export function createResource( resolved ? "ready" : "unresolved" ); + if (Owner) + onCleanup(() => { + for (const c of contexts.keys()) c.decrement!(); + contexts.clear(); + if (Transition && pr) Transition.promises.delete(pr); + pr = null; + }); + if (sharedConfig.context) { id = sharedConfig.getNextContextId(); if (options.ssrLoadFrom === "initial") initP = options.initialValue as T; diff --git a/packages/solid/src/render/component.ts b/packages/solid/src/render/component.ts index afd807eff..5775162f8 100644 --- a/packages/solid/src/render/component.ts +++ b/packages/solid/src/render/component.ts @@ -3,6 +3,7 @@ import { createSignal, createResource, createMemo, + onCleanup, devComponent, $PROXY, SUPPORTS_PROXY, @@ -356,7 +357,7 @@ export function splitProps< export function lazy>( fn: () => Promise<{ default: T }> ): T & { preload: () => Promise<{ default: T }> } { - let comp: () => T | undefined; + let comp: (() => T | undefined) | undefined; let p: Promise<{ default: T }> | undefined; const wrap: T & { preload?: () => void } = ((props: any) => { const ctx = sharedConfig.context; @@ -374,10 +375,11 @@ export function lazy>( } else if (!comp) { const [s] = createResource(() => (p || (p = fn())).then(mod => mod.default)); comp = s; + onCleanup(() => (comp = undefined)); } let Comp: T | undefined; return createMemo(() => - (Comp = comp()) + (Comp = comp?.()) ? untrack(() => { if (IS_DEV) Object.assign(Comp!, { [$DEVCOMP]: true }); if (!ctx || sharedConfig.done) return Comp!(props);