Skip to content
All articles

Engineering

Making the bid queue fast with virtualized lists

2 MINUTES READ

Shawn Azdam

Founder

Schematic interface grid with a glowing viewport band over dark slate.
On this page

The bid queue renders up to five thousand packages with live fit scores, maps thumbnails, and status pills. Last month it ran at 4 frames per second on a mid-range laptop, and scrolling felt like dragging the page through mud. The fix was virtualization: render only the rows you can see. This is the full story, including the two traps that bit us.

Measuring first

Before touching code, we profiled. The breakdown was unambiguous:

  • 78% of frame time in DOM layout and paint for off-screen rows
  • 14% in fit-score recomputation on scroll
  • 8% in everything else

Layout dominated because every row mounted a map thumbnail, three pills, and a sparkline — times five thousand. No amount of memoization fixes rendering five thousand of anything. The only fix is rendering fewer.

The virtualizer

The approach is standard but worth stating precisely. The list container keeps its full scroll height via a spacer element, and we mount only the rows intersecting the viewport plus a small overscan buffer:

const ROW_HEIGHT = 96; // fixed row geometry, measured once
const OVERSCAN = 6; // rows above and below the viewport

function visibleRange(scrollTop, viewportHeight, total) {
	const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
	const end = Math.min(total, Math.ceil((scrollTop + viewportHeight) / ROW_HEIGHT) + OVERSCAN);
	return { start, end };
}

Fixed row geometry is the load-bearing decision. Variable heights require measuring every row, which reintroduces the layout cost we were escaping. We redesigned the row to a fixed 96 pixels instead — a better row and a faster list.

Trap one: scroll-linked recomputation

Our first version recomputed fit scores during scroll handling, which re-triggered renders mid-scroll and halved the gain. The fix: scores compute once per data change and are cached by package id; scroll handlers only slice the already-scored array. Scrolling is now pure DOM recycling — no business logic runs between frames.

Trap two: focus and keyboard navigation

Virtualized lists eat keyboard users if you let rows unmount beneath focus. We keep the focused row mounted regardless of scroll position and restore focus by package id after re-renders. The automated check is simple: tab through the queue with a screen reader and confirm every control announces. It passes, and we run it on every release.

The numbers

MetricBeforeAfter
Scroll fps (mid-range laptop)460
Mounted rows (5,000 total)5,000~30
Interaction to paint900ms60ms

Do fewer things. It works every time.

Orphan-well intel, monthly

Contact sales