Context
Discord report: "animations are no longer working" on examples/component/helix. Root-caused. The animation loss is a symptom; the defect is a per-move forced layout that makes DOM reordering quadratic.
Reproduce: npm run server-start, open examples/component/helix/index.html, set the amount field to 600 (sort is disabled below that — buffered-store sorting is unimplemented), click Sort by Firstname.
Not implicated: the VDom engine, delta throughput, data.Store, collections
Stated up front because the symptom invites all four. Delta throughput is healthy right now — mouse-wheel rotation on the same 600 items sustains ~70k delta updates/sec (600 transform-matrix updates per frame), and the helix exists precisely as that stress demo. In the instrumented sort run, updateNode cost 7ms across 1182 calls.
Rotation is unaffected because it only ever updates transforms. It never moves a node, and the cost below is entirely in node movement.
Root Cause
src/main/DeltaUpdates.mjs:631-633, inside moveNode's nativeMoveBefore branch:
parentNode.style.display = 'none';
void parentNode.offsetHeight;
parentNode.style.display = displayValue;
A forced synchronous layout on every single moveNode call. Its cost scales with the parent's child count, so reordering N siblings costs O(N²). In the helix the parent is .group — 590 3D-transformed children under transform-style: preserve-3d + perspective: 800px — so each move tears down and rebuilds that entire 3D box tree.
Measured causation, same page, same sort, toggling only DeltaUpdates.nativeMoveBefore:
| path |
moveNode total |
main-thread long tasks |
| heal enabled (current) |
8137ms |
one, 8133ms |
insertBefore fallback (no heal) |
18ms |
none |
452×. Wall-clock varies run to run (6.6s–8.1s observed); the fallback comparison is the stable signal.
Origin
Introduced in ac231036b5 (2026-06-11), "fix(grid): heal mid-drag cell-mapping staleness + Chromium moveBefore stale flex layout" — #12894, closing #12883. A grid column-drag fix, applied to every moveNode rather than scoped to the case it repaired.
The window is therefore ~8 weeks, not the "possibly a year" the report's age suggested.
The code comment at :624 refers to a heal-gating follow-up; no such ticket is on the board, so this ticket carries that tracking.
Blast Radius — by construction, measured only on the helix
moveNode is the shared path for every sibling reorder, so by construction this cost applies to any consumer moving many siblings within one parent: grids, lists, trees, drag-and-drop, sorted store-driven views. Measured only on the helix so far — the helix makes it visible by moving 590 nodes at once inside a 3D scene. Quantifying it on a grid is worth doing but is not evidence this ticket currently holds.
The Animation Loss (the reported symptom)
applyItemTransitions (src/component/Helix.mjs:462) adds neo-transition-1000, runs the callback, then schedules class removal via setTimeout(animationTime + 200) — a wall-clock timer anchored to the class addition. The long task starves it, so it fires immediately on release. Measured in one run: transform applied at 6780ms, class removed at 6793ms — 13ms later, so transition-duration drops to 0s before the browser renders a frame. Items snap.
Fixing the heal restores the animation incidentally, but the timer stays anchored to the wrong event and will lose again on any slow frame. Worth fixing separately, anchored to the transform application or transitionend.
The Fix
- Batch the heal. It runs per move but only needs to run once per
update() batch: no paint occurs between deltas inside a single task, so the intermediate rebuilds are unobservable. This alone converts O(N²) to O(N) while preserving the repaired end state.
- Consider gating it.
#12894 addressed a stale flex sibling chain; applying it to every parent, including non-flex and 3D-transformed containers, is broader than the defect it fixed.
- Re-anchor the transition window in
Helix.applyItemTransitions — independent of 1 and 2.
Acceptance Criteria
Out of Scope
- Sorting for buffered stores (why the buttons are disabled below 600).
neo-transition-600 is added at Helix.mjs:747 / :923-929 while Helix.scss defines only -100 and -1000, so that class matches no rule. Separate small ticket.
Decision Record impact
none — a performance correction inside an existing main-thread addon.
Related
#12894 / #12883 (origin of the heal), #8620 (Element.moveBefore adoption).
Context
Discord report: "animations are no longer working" on
examples/component/helix. Root-caused. The animation loss is a symptom; the defect is a per-move forced layout that makes DOM reordering quadratic.Reproduce:
npm run server-start, openexamples/component/helix/index.html, set the amount field to600(sort is disabled below that — buffered-store sorting is unimplemented), click Sort by Firstname.Not implicated: the VDom engine, delta throughput,
data.Store, collectionsStated up front because the symptom invites all four. Delta throughput is healthy right now — mouse-wheel rotation on the same 600 items sustains ~70k delta updates/sec (600 transform-matrix updates per frame), and the helix exists precisely as that stress demo. In the instrumented sort run,
updateNodecost 7ms across 1182 calls.Rotation is unaffected because it only ever updates transforms. It never moves a node, and the cost below is entirely in node movement.
Root Cause
src/main/DeltaUpdates.mjs:631-633, insidemoveNode'snativeMoveBeforebranch:parentNode.style.display = 'none'; void parentNode.offsetHeight; // forced synchronous reflow parentNode.style.display = displayValue;A forced synchronous layout on every single
moveNodecall. Its cost scales with the parent's child count, so reordering N siblings costs O(N²). In the helix the parent is.group— 590 3D-transformed children undertransform-style: preserve-3d+perspective: 800px— so each move tears down and rebuilds that entire 3D box tree.Measured causation, same page, same sort, toggling only
DeltaUpdates.nativeMoveBefore:moveNodetotalinsertBeforefallback (no heal)452×. Wall-clock varies run to run (6.6s–8.1s observed); the fallback comparison is the stable signal.
Origin
Introduced in
ac231036b5(2026-06-11), "fix(grid): heal mid-drag cell-mapping staleness + Chromium moveBefore stale flex layout" —#12894, closing#12883. A grid column-drag fix, applied to everymoveNoderather than scoped to the case it repaired.The window is therefore ~8 weeks, not the "possibly a year" the report's age suggested.
The code comment at
:624refers to a heal-gating follow-up; no such ticket is on the board, so this ticket carries that tracking.Blast Radius — by construction, measured only on the helix
moveNodeis the shared path for every sibling reorder, so by construction this cost applies to any consumer moving many siblings within one parent: grids, lists, trees, drag-and-drop, sorted store-driven views. Measured only on the helix so far — the helix makes it visible by moving 590 nodes at once inside a 3D scene. Quantifying it on a grid is worth doing but is not evidence this ticket currently holds.The Animation Loss (the reported symptom)
applyItemTransitions(src/component/Helix.mjs:462) addsneo-transition-1000, runs the callback, then schedules class removal viasetTimeout(animationTime + 200)— a wall-clock timer anchored to the class addition. The long task starves it, so it fires immediately on release. Measured in one run: transform applied at 6780ms, class removed at 6793ms — 13ms later, sotransition-durationdrops to 0s before the browser renders a frame. Items snap.Fixing the heal restores the animation incidentally, but the timer stays anchored to the wrong event and will lose again on any slow frame. Worth fixing separately, anchored to the transform application or
transitionend.The Fix
update()batch: no paint occurs between deltas inside a single task, so the intermediate rebuilds are unobservable. This alone converts O(N²) to O(N) while preserving the repaired end state.#12894addressed a stale flex sibling chain; applying it to every parent, including non-flex and 3D-transformed containers, is broader than the defect it fixed.Helix.applyItemTransitions— independent of 1 and 2.Acceptance Criteria
#12883stays fixed: a grid column drag to the locked-end region still re-homes correctly.Out of Scope
neo-transition-600is added atHelix.mjs:747/:923-929whileHelix.scssdefines only-100and-1000, so that class matches no rule. Separate small ticket.Decision Record impact
none— a performance correction inside an existing main-thread addon.Related
#12894/#12883(origin of the heal),#8620(Element.moveBeforeadoption).