AniUI Academy

Compute a virtual list's visible range

mediumDOM & Utilities+250 XPPractice

Rendering ten thousand rows kills a page, so virtualised lists render only the rows in view plus a small buffer, and offset them so the scrollbar still behaves. All of that is one small pure calculation, and it is worth getting exactly right — off by one here and rows flicker at the edges.

Implement visibleRange({ scrollTop, rowHeight, viewportHeight, totalRows, overscan = 0 }) returning { start, end, offsetTop }, where start and end are row indices and end is **inclusive**.

Treat a negative scrollTop as 0, since overscroll bounce can produce one. Then start is Math.floor(scrollTop / rowHeight) - overscan, clamped to at least 0 and at most totalRows - 1. And end is Math.ceil((scrollTop + viewportHeight) / rowHeight) - 1 + overscan, clamped to at most totalRows - 1 and never less than start. offsetTop is start * rowHeight — the padding or translateY that puts the first rendered row in the right place.

When totalRows is 0 there is nothing to render, so return exactly { start: 0, end: -1, offsetTop: 0 }.

Arguments

options{ scrollTop: number; rowHeight: number; viewportHeight: number; totalRows: number; overscan?: number }
The pixel-space inputs from the scroller: current scroll offset, per-row height, visible height, total row count, and an optional buffer of extra rows to render on each side.

Returns

{ start: number; end: number; offsetTop: number } — The inclusive row index range to render plus the pixel offset to apply so the first rendered row sits in the right place.

Examples

visibleRange({ scrollTop: 0, rowHeight: 40, viewportHeight: 400, totalRows: 1000 });
// => { start: 0, end: 9, offsetTop: 0 }

At the top of a list of 40px rows, ten rows cover 400px of viewport.

visibleRange({ scrollTop: 100, rowHeight: 40, viewportHeight: 400, totalRows: 1000 });
// => { start: 2, end: 12, offsetTop: 80 }

A partial row at each edge means eleven rows render, offset by two.

visibleRange({ scrollTop: 400, rowHeight: 40, viewportHeight: 400, totalRows: 1000, overscan: 3 });
// => { start: 7, end: 22, offsetTop: 280 }

Overscan adds three extra rows above and below the strict range.

What it has to do

  • Return { start, end, offsetTop } with an inclusive end.
  • Subtract overscan from the start and add it to the end, defaulting overscan to 0.
  • Clamp both indices into 0 .. totalRows - 1 and never let end fall below start.
  • Compute offsetTop as start * rowHeight.
  • Return { start: 0, end: -1, offsetTop: 0 } when totalRows is 0.

Notes

  • end is **inclusive** so a caller can write for (let i = start; i <= end; i += 1) without off-by-one worries. The { start: 0, end: -1 } sentinel for the empty case makes that loop iterate zero times.
  • offsetTop is what you apply to the container of the rendered rows — either as padding-top or as a translateY — so the scrollbar still thinks the list is full height.
Asked atNetflixAirbnb

Stuck? The javascript course covers everything this challenge needs.

Try it yourself
Loading playground...
5 tests · +250 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.