ITADN

[Bug]: Severe edit-mode lag with many children — children re-mounted on every keystroke due to object-identity keying

#1048Openduczz 创建于 2026-05-28
D
duczzcommented
## Environment | | | |---|---| | Home Assistant | 2026.5.4 | | expander-card | latest (main) | | Browser | Chrome | ## Describe the bug When using `custom:expander-card` with many child cards, the dashboard editor becomes extremely slow — every keystroke in the YAML or visual editor causes a multi-second freeze. **Measured INP: ~6,667 ms** (well above the 200 ms "Good" threshold). ## Root cause (code analysis) ### Bug 1 — Object-identity keying in `ExpanderCard.svelte` (main cause) ```svelte {#each config.cards as card (card)} ``` The key expression `(card)` uses **object identity**. Home Assistant's edit flow works like this: 1. User edits anything → HA serializes the full config to YAML 2. HA parses YAML back → **all card objects are new instances** 3. Svelte compares keys by identity → sees 100% new objects 4. Svelte **destroys and re-mounts every child card** on every keystroke With 18 child cards this means 18 full mount/unmount cycles per keystroke. ### Bug 2 — ResizeObserver leak in `Card.svelte` ```svelte onMount(() => { const ro = new ResizeObserver(...); ro.observe(container); // no onDestroy(() => ro.disconnect()) }); ``` Each re-mount (triggered by Bug 1) creates a new `ResizeObserver` that is never disconnected. Observers accumulate across re-mounts, each firing on every resize. ### Bug 3 — `$effect` blocks without value-equality guard Three `$effect` blocks assign to DOM properties unconditionally: ```js $effect(() => { container.hass = hass; }); $effect(() => { container.preview = preview; }); $effect(() => { cardConfig.disabled = !open; }); ``` When all children re-mount, these effects re-run immediately, adding to the cost. ## Steps to reproduce 1. Create an `expander-card` with 15+ child cards 2. Open the dashboard editor 3. Click into the YAML editor or change any property 4. Observe multi-second freeze on every edit ## Expected behavior Edit-mode should remain responsive. Children should only re-mount when their own config actually changes. ## Suggested fix Replace the object-identity key with a **stable per-position key** — the same pattern used by HA's own `hui-stack-card-editor` and by [ha-stack-in-card](https://github.com/user/ha-stack-in-card): ```svelte <script> // Generate stable keys once per position; survive config roundtrips let keys = $state([]); $effect(() => { const newLen = config.cards?.length ?? 0; if (newLen > keys.length) { keys = [...keys, ...Array.from({ length: newLen - keys.length }, () => Math.random().toString(36).slice(2))]; } else if (newLen < keys.length) { keys = keys.slice(0, newLen); } }); </script> {#each config.cards as card, i (keys[i])} <Card {card} ... /> {/each} ``` Also fix the ResizeObserver leak: ```svelte onMount(() => { const ro = new ResizeObserver(...); ro.observe(container); onDestroy(() => ro.disconnect()); // add this }); ``` ## Additional context The cascade per keystroke (with 18 children): - 18× child card destroy + re-mount - 18× `JSON.parse(JSON.stringify(config))` deep-clone - 18× new `ResizeObserver` (never cleaned up) - If `card_mod` is used: 18× `new Function()` style template re-evaluation (also triggers CSP warnings)
3 条评论