Summary
Hi!
While working with Svelte 5 in an SPA setup, we noticed detached DOM nodes accumulating over time after components are destroyed.
In our case, repeatedly mounting/unmounting a component like:
{#if chatId}
<Chat />
{/if}
eventually leads to a noticeable number of detached elements (e.g. HTMLVideoElement, wrappers, buttons) remaining in memory. After several open/close cycles, this can grow quite large.
In heap snapshots, these nodes appear with detachedness != 0, often retained via system / Context (closure scope). This seems consistent with the behavior described in Chrome DevTools documentation:
https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#uncover_dom_leaks
— where a reference to a single node can keep the whole detached subtree alive.
What might be happening
From what we can see, Svelte correctly removes nodes from the document (node.remove()) and clears effect internals.
However, the detached subtree itself remains internally connected (via parent/child relationships). If any closure still references one of those nodes (for example via bind:, event handlers, or template locals), the entire subtree may remain reachable.
Since compiled templates keep multiple DOM references within a shared closure, it’s not straightforward to fully clean this up at the application level.
Reproduction
Repo: https://github.com/dimensi/svelte-dom-leak-repro
- SvelteKit SPA (
ssr = false)
- Run:
npm install && npm run dev
Steps:
- Take heap snapshot (A)
- Interact with rows (play/pause, counters)
- Click “Stress 600 remounts”
- Click garbage collect in devtoos
- Take snapshot (B)
Detached DOM nodes tend to increase, especially after interactions.
Possible direction (just for discussion)
One idea could be to extend teardown so that detached subtrees are not only removed from the document, but also fully disconnected internally, for example:
- Clearing delegated event handler storage (
event_symbol)
- Resetting media elements (
video/audio)
- Recursively removing children to break parent/child chains
This might help avoid situations where a single lingering reference retains the entire subtree.
Experimental patch (PoC)
This is just a proof of concept that helped in our case (significantly reduced detached nodes), not necessarily a suggested final solution:
export function remove_effect_dom(node, end) {
while (node !== null) {
var next = node === end ? null : get_next_sibling(node);
node.remove();
deep_cleanup_node(node);
node = next;
}
}
function deep_cleanup_node(node) {
while (node.firstChild) {
deep_cleanup_node(node.firstChild);
node.firstChild.remove();
}
}
Environment
- Svelte 5.x (observed on 5.55.x)
- Chrome DevTools heap snapshots
Summary
Hi!
While working with Svelte 5 in an SPA setup, we noticed detached DOM nodes accumulating over time after components are destroyed.
In our case, repeatedly mounting/unmounting a component like:
{#if chatId} <Chat /> {/if}eventually leads to a noticeable number of detached elements (e.g.
HTMLVideoElement, wrappers, buttons) remaining in memory. After several open/close cycles, this can grow quite large.In heap snapshots, these nodes appear with
detachedness != 0, often retained viasystem / Context(closure scope). This seems consistent with the behavior described in Chrome DevTools documentation:https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots#uncover_dom_leaks
— where a reference to a single node can keep the whole detached subtree alive.
What might be happening
From what we can see, Svelte correctly removes nodes from the document (
node.remove()) and clears effect internals.However, the detached subtree itself remains internally connected (via parent/child relationships). If any closure still references one of those nodes (for example via
bind:, event handlers, or template locals), the entire subtree may remain reachable.Since compiled templates keep multiple DOM references within a shared closure, it’s not straightforward to fully clean this up at the application level.
Reproduction
Repo: https://github.com/dimensi/svelte-dom-leak-repro
ssr = false)npm install && npm run devSteps:
Detached DOM nodes tend to increase, especially after interactions.
Possible direction (just for discussion)
One idea could be to extend teardown so that detached subtrees are not only removed from the document, but also fully disconnected internally, for example:
event_symbol)video/audio)This might help avoid situations where a single lingering reference retains the entire subtree.
Experimental patch (PoC)
This is just a proof of concept that helped in our case (significantly reduced detached nodes), not necessarily a suggested final solution:
Environment