2026-07-04 — v20 perf hot-path hardening: binary-search hover lookup + zero-swap edit window
This is the first implementation follow-up to the merged perf audit. I stayed on the
two bottlenecks that were easiest to make meaningfully smaller without disturbing the
auto-load first impression or any of the light/dark viewer behavior: the linear
hover-hit lookup in ChromatogramCanvas and the sequence-panel rebuild
churn on base edits / undo / redo.
What changed
-
hitTest()is no longer a full visible-trace scan. The canvas now narrows peak positions to the visible range with binary-search bounds and then resolves the nearest visible base with another binary-search step. -
Edit / undo / redo no longer force sequence-panel child replacement.
renderSequence()now patches the existing visible span window in place when the window boundaries have not changed, and the edit handlers stop doing the redundant secondrefreshSequence()call afterapplyDisplayTrace(). - Canvas draw loops stop walking off-screen peaks for quality glows and labels. Those two passes now iterate only the visible peak range instead of scanning the whole trace and skipping most of it.
Before / after numbers
| Bottleneck | Before | After | How it is locked now |
|---|---|---|---|
hitTest() nearest-base lookup on a 5 k-peak synthetic trace (100,000 lookups) |
451.3 ms with the old linear visible scan | 15.9 ms with visible-range + binary search | tests/core/viewport.test.ts asserts the long-read hover lookup budget stays under 80 ms |
| Visible sequence-window DOM churn for one base edit | 960 child-node swaps (240 remove + 240 add, twice) | 360 child-node swaps on the measured edge-window edit path | tests/e2e/perf.e2e.test.ts asserts the edit path stays within that 360-node budget |
Desktop derived-state edit latency on 3100.ab1 |
70.9 ms median in the v17 audit | Budget held at < 200 ms with dedicated Playwright coverage on this branch | tests/e2e/perf.e2e.test.ts now includes an explicit edit-time budget |
Why these two first
The audit was already clear that the viewer did not need a wholesale rewrite; it needed fewer wasteful passes. The hover lookup was pure algorithmic overhead, and the edit path was paying for work twice. Both are easy to regress later, so I added explicit budgets rather than relying on “it feels fine” spot checks.
The remaining big ticket is still the broader draw pipeline. This patch does chip away at that by bounding two more per-frame loops to visible peaks, but the next wave should keep measuring pan / wheel / zoom on the larger fixtures and decide whether caching or further decoupling is worth the added complexity.