LearnNewsExamplesServices
Frontmatter
id16538
titleHelix sorts the DOM twice and cancels its own transition window
stateClosed
labels
bug
assigneesneo-opus-grace
createdAtAug 5, 2026, 12:41 PM
updatedAtAug 5, 2026, 7:01 PM
githubUrlhttps://github.com/neomjs/neo/issues/16538
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 5, 2026, 7:01 PM

Helix sorts the DOM twice and cancels its own transition window

neo-opus-grace
neo-opus-grace commented on Aug 5, 2026, 12:41 PM

Context

#16533 fixed the DeltaUpdates half of the helix sort regression (per-move layout heal → batched + skipped mid-transition, PR #16534, merged). It closed with three Helix-side findings explicitly deferred. This ticket carries them so they do not retire with it.

Operator, after testing the merged fix on a visible browser: "first sort button click has an animation, but still VERY slow compared to e.g. a year ago". The heal was the animation killer; the items below are the remaining cost and the remaining fragility.

1. Every sort reorders the DOM twice

Helix.sortItems (src/component/Helix.mjs:1059) hand-writes one moveNode delta per item straight to Neo.applyDeltas, bypassing the VDom worker. The differ never sees those moves, so it emits its own reorder afterwards. Measured on 590 items — three batches per sort:

t batch contents
~20ms 1 {moveNode: 590}sortItems' manual deltas
~50ms 2 {moveNode: 586, updateNode: 590} — the differ's own reorder
~82ms 3 {updateNode: 590}

~1176 moves where 590 would do. Node reordering is the expensive operation here — transform updates are not (updateNode cost 7ms across 1182 calls in the same run, which is why mouse-wheel rotation sustains ~70k deltas/sec untouched).

Falsified while investigating, so nobody re-chases it: this is not vdom/DOM order divergence. Probing each item's matrix rotation component by DOM index gave a constant 13 direction changes before and after both sorts — that count is the spiral wrapping, not disorder. The order is correct; the work is duplicated.

Worth asking whether the manual moveNode pass is needed at all now, given the differ reorders anyway.

2. The transition window is anchored to the wrong event

applyItemTransitions (:462) adds neo-transition-1000, invokes the callback, then schedules class removal with setTimeout(animationTime + 200) — a wall-clock timer started when the class is added. But the transition can only begin when the transform is applied, after an unbounded amount of work.

Before #16533 this lost outright: the transform landed at 6780ms and the class was removed at 6793ms, 13ms later. With the heal batched it currently wins with ~1134ms of margin — but nothing enforces that, and any slow frame reintroduces the failure. The window should be anchored to the transform application or to transitionend, not to the class add.

3. neo-transition-600 matches no CSS rule

Added at Helix.mjs:747 and :923-929; resources/scss/src/component/Helix.scss defines only neo-transition-100 and neo-transition-1000. Either the rule is missing or the class is dead — both are cheap to resolve and one of them is a silent no-op today.

Acceptance Criteria

  • A sort issues one reorder pass, not two — asserted on delta counts per batch, which fails against the current duplicate pass.
  • The transition class outlives the transform application by at least one rendered frame by construction, not by timing luck.
  • neo-transition-600 either matches a rule or is removed; no class is added that nothing styles.
  • Sorting 600 items animates smoothly on repeated sorts, confirmed on a visible browser — see the bound below.

Verification bound, stated because it cost a wrong claim on #16533

The animation result cannot be verified from the agent harness. The Claude browser pane reports document.hidden: true even when fronted, and background tabs neither composite nor run transitions nor fire transition events, while throttling timers to ~1/sec. JS-timing evidence proves only a necessary condition. npm run test-components renders properly and is the right home for anything animation-dependent; final visual confirmation is an operator/visible-tab gate.

Out of Scope

  • DeltaUpdates — fixed in #16533 / PR #16534; its regression guard is #16536.
  • Sorting for buffered stores, which is why the sort buttons are disabled below 600 items.

Decision Record impact

none — component-internal.

Related

#16533 / PR #16534 (the merged half) · #16536 (its guard) · #12894 / #12883 (heal origin).

tobiu referenced in commit 7fdbe11 - "fix(component): a helix sort reorders the DOM once, not twice (#16538) (#16552) on Aug 5, 2026, 7:01 PM
tobiu closed this issue on Aug 5, 2026, 7:01 PM