[Bug]: Severe edit-mode lag with many children — children re-mounted on every keystroke due to object-identity keying
## 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 条评论