Virtual list

가상 리스트

A technique for long lists that renders only the rows currently in view (plus a small buffer) as real DOM nodes.

Also known as: Windowed listVirtualized listWindowing
···
html
<div class="vlw">
  <div class="vlmeta">렌더된 행: <b id="vlcount">0</b> / 10,000</div>
  <div class="vlvp" id="vlvp">
    <div class="vlsp" id="vlsp"><div class="vlrows" id="vlrows"></div></div>
  </div>
</div>
css
.vlw{position:absolute;inset:12px;display:flex;flex-direction:column;gap:5px}
.vlmeta{flex:none;font-size:10px;color:var(--muted)}
.vlmeta b{color:var(--accent);font-variant-numeric:tabular-nums}
.vlvp{flex:1;min-height:0;overflow-y:auto;border:1px solid var(--line);border-radius:8px;background:var(--surface)}
.vlsp{position:relative}
.vlrows{position:absolute;left:0;right:0;top:0}
.vlrow{height:24px;display:flex;align-items:center;padding:0 9px;border-bottom:1px solid var(--line);font-size:10px;color:var(--fg)}
.vlrow:nth-child(even){background:color-mix(in srgb, var(--line) 35%, transparent)}
js
const ROWH = 24, COUNT = 10000, BUFFER = 3;
const vp = document.getElementById('vlvp'), sp = document.getElementById('vlsp'), rows = document.getElementById('vlrows'), countEl = document.getElementById('vlcount');
sp.style.height = (COUNT * ROWH) + 'px';
function render() {
  const top = vp.scrollTop, viewH = vp.clientHeight || 140;
  const start = Math.max(0, Math.floor(top / ROWH) - BUFFER);
  const end = Math.min(COUNT, Math.ceil((top + viewH) / ROWH) + BUFFER);
  rows.style.transform = 'translateY(' + (start * ROWH) + 'px)';
  let html = '';
  for (let i = start; i < end; i++) html += '<div class="vlrow">#' + (i + 1) + ' — row item</div>';
  rows.innerHTML = html;
  countEl.textContent = String(end - start);
}
vp.addEventListener('scroll', render);
render();
let dir = 1;
setInterval(() => {
  const max = sp.clientHeight - vp.clientHeight;
  vp.scrollTop = Math.max(0, Math.min(max, vp.scrollTop + dir * 260));
  if (vp.scrollTop <= 0 || vp.scrollTop >= max) dir *= -1;
}, 500);

Rendering every row of a list with thousands of entries makes the browser stutter. A virtual list looks at the scroll position and renders only the handful of rows currently visible (plus a small buffer) as real DOM elements, treating the rest as if they didn't exist. To fake the full scrollable length, an invisible "spacer" container is sized to the true total height (row count × row height), and only the visible rows are placed inside it via translateY.

The point of the counter in the demo below is that it hovers around 10–20 no matter how far you scroll — the data has 10,000 rows, but the DOM only ever holds as many as fit on screen. Compared with pagination, pagination shows the user an explicit "page" number and jumps between them on click, while a virtual list scrolls smoothly as one continuous list with no page boundaries, swapping rows only internally.

Variable row heights make this much harder — scroll position alone can't tell you which row index you're at, so you need separate logic caching each row's measured height. For screen reader users, giving the container aria-setsize/aria-posinset-style hints about the total count and current position helps make sense of a list that isn't fully in the DOM.