Skip to content

Instantly share code, notes, and snippets.

@dimensi
Created April 11, 2026 14:09
Show Gist options
  • Select an option

  • Save dimensi/98dc8ef4c9820c6452a504519cec0bb8 to your computer and use it in GitHub Desktop.

Select an option

Save dimensi/98dc8ef4c9820c6452a504519cec0bb8 to your computer and use it in GitHub Desktop.
Svelte 5 detached DOM repro (minimal)
<script lang="ts">
import LeakyBlock from '$lib/LeakyBlock.svelte';
let key = $state(0);
let batch = $state(0);
function remount() {
key += 1;
}
function remountMany() {
const n = 40;
for (let i = 0; i < n; i++) {
key += 1;
}
batch += 1;
}
</script>
<svelte:head>
<title>Svelte 5 — detached DOM repro</title>
</svelte:head>
<main>
<h1>Detached DOM repro (Svelte 5)</h1>
<p>
Destroy/remount the block with <code>&#123;#key&#125;</code>. Then in Chrome DevTools → Memory → Heap snapshot,
filter by <strong>Detached</strong> and look for <code>HTMLVideoElement</code> / <code>HTMLDivElement</code> (class
<code>card</code>).
</p>
<p>
Run with <code>chrome --js-flags="--expose-gc"</code> if you want to call <code>gc()</code> from the console between
snapshots.
</p>
<div class="actions">
<button type="button" onclick={remount}>Remount once (key += 1)</button>
<button type="button" onclick={remountMany}>Remount 40× (batch {batch})</button>
</div>
<p>Current key: <strong>{key}</strong></p>
{#key key}
<LeakyBlock />
{/key}
</main>
<style>
main {
font-family: system-ui, sans-serif;
max-width: 52rem;
margin: 1rem auto;
padding: 0 1rem;
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin: 1rem 0;
}
code {
background: #f0f0f0;
padding: 0.1em 0.35em;
border-radius: 4px;
}
</style>

Svelte 5 — detached DOM heap repro

Minimal SvelteKit app to reproduce accumulation of detached DOM nodes after mount/destroy cycles when the compiled template holds multiple DOM references (wrapper + <video> + controls) in shared closure scopes.

Run

npm install
npm run dev

Open the app, click “Remount 40×” several times (or “Remount once” repeatedly).

Observe (Chrome DevTools)

  1. Open MemoryHeap snapshot → take snapshot A (baseline).
  2. Click Remount 40× a few times (e.g. 5× = 200 teardowns).
  3. Optionally run gc() in the console (Chrome must be started with --js-flags="--expose-gc").
  4. Take snapshot B.
  5. In snapshot B, filter by Detached and search for HTMLVideoElement, HTMLDivElement, or data-leak-repro.

Related reading

Chrome describes the underlying DOM retention pattern: a JS reference to any node in a detached subtree keeps the entire subtree alive via parentNode / child links until that reference is dropped.

What we tested upstream

Clearing video.src / load() from app onDestroy did not fix large detached counts in a real app; breaking parent–child links after remove() in Svelte’s remove_effect_dom (runtime) did (~99% reduction in detached delta in that scenario). See the GitHub issue for details.

<script lang="ts">
/**
* Intentionally mirrors patterns that compile to multiple DOM locals in one
* template closure: wrapper + media + controls + reactive reads.
*/
let paused = $state(true);
let duration = $state(0);
let clicks = $state(0);
</script>
<div class="card" data-leak-repro>
<video bind:paused bind:duration muted loop playsinline>
<!-- no remote URL: keeps repro offline; binding/effect graph is what matters -->
</video>
<button type="button" onclick={() => (clicks += 1)} aria-label="noop">{clicks}</button>
<button type="button" onclick={() => (paused = !paused)}>
{paused ? 'play' : 'pause'}
</button>
</div>
<style>
.card {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
border: 1px solid #ccc;
}
video {
width: 120px;
height: 68px;
background: #222;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment