Mastering Virtual Lists in Vue: From Fixed Height to Dynamic Items

This article explains why virtual lists are needed for long scrollable data, defines the concept, walks through a step‑by‑step implementation for fixed‑height items, extends the solution to handle variable heights, and shares practical tips and code snippets for Vue developers.

HelloTech
HelloTech
HelloTech
Mastering Virtual Lists in Vue: From Fixed Height to Dynamic Items

Why Use a Virtual List

In the Hello Goods Mall, product pages often contain long lists such as waterfall flows or special‑sale venues. When the user scrolls to the bottom, new data is loaded, causing the DOM to grow and the page to become sluggish. A virtual list renders only the items visible in the viewport, keeping the DOM size constant and the UI smooth.

What Is a Virtual List?

A virtual list does not render the entire data set; it only renders a slice of the data that fills the visible area. For example, if the screen can display three cards but thousands of items exist, only the three visible cards are rendered and swapped out as the user scrolls.

Implementing a Fixed‑Height Virtual List

We start with a simple demo that loads 20 items per page and appends more when the user reaches the bottom. Assuming a viewport height of 1000 px and each item height of 100 px, ten items are visible at any time.

When scrollTop is 0, the rendered slice is listData.slice(0, 10). After scrolling 100 px, the slice becomes listData.slice(1, 11). In general:

visibleCount = Math.ceil(scrollContainerHeight / itemHeight) startIndex = Math.floor(scrollTop / itemHeight) endIndex = startIndex + visibleCount

visibleList = listData.slice(startIndex, endIndex) scrollOffset = startIndex * itemHeight

Key variables:

scrollContainerHeight – height of the scrolling container

itemHeight – fixed height of each list item

visibleCount – number of items that fit in the viewport

scrollTop – current scroll distance

listData – the full data array

visibleList – the slice actually rendered

startIndex, endIndex – indices of the slice

scrollOffset – vertical offset applied to the wrapper

<template>
  <div @scroll="scrollEvent($event)" class="list-container">
    <div :style="{height: `${totalHeight}px`, transform: `translateY(${scrollOffset}px)`}" class="list-wrapper">
      <div v-for="item in visibleList" :key="item" class="list-item">{{ item }}</div>
    </div>
  </div>
</template>
const totalHeight = computed(() => {
  return listData.value.length * itemHeight.value;
});

const scrollEvent = (e) => {
  const scrollTop = e.target.scrollTop;
  startIndex.value = Math.floor(scrollTop / itemHeight.value);
  endIndex.value = startIndex.value + visibleCount.value;
  visibleList.value = listData.value.slice(startIndex.value, endIndex.value);
  scrollOffset.value = startIndex.value * itemHeight.value;
};

The demo shows that regardless of the total number of items, only ten DOM nodes are ever rendered.

Handling Variable‑Height Items

When item heights differ (e.g., a product waterfall layout), the fixed‑height calculation causes jitter. The solution is to keep an estimated height (e.g., 50 px) and maintain a positions array storing each item's actual height, top, and bottom. After rendering, we measure each item's real height and update the array.

const updateItemHeight = () => {
  const nodes = visibleItemRef.value;
  nodes.forEach((node) => {
    if (!node) return;
    const rect = node.getBoundingClientRect();
    const id = node.id;
    const oldHeight = positions.value[id].height;
    const currentHeight = rect.height;
    const diffHeight = oldHeight - currentHeight;
    if (diffHeight !== 0) {
      positions.value[id].height = currentHeight;
      positions.value[id].bottom -= diffHeight;
    }
  });
  const startId = +nodes[0].id;
  for (let i = startId + 1; i < positions.value.length; i++) {
    positions.value[i].top = positions.value[i - 1].bottom;
    positions.value[i].bottom = positions.value[i].top + positions.value[i].height;
  }
};

During scrolling, we locate the first item whose bottom exceeds scrollTop and set it as startIndex. The offset becomes the bottom of the previous item.

const scrollEvent = (e) => {
  const scrollTop = e.target.scrollTop;
  const startItem = positions.value.find(p => p.bottom > scrollTop);
  startIndex.value = startItem ? startItem.index : 0;
  endIndex.value = startIndex.value + visibleCount.value;
  visibleList.value = listData.value.slice(startIndex.value, endIndex.value);
  scrollOffset.value = startIndex.value > 0 ? positions.value[startIndex.value - 1].bottom : 0;
};

Practical Optimizations and Real‑World Use

The basic demos can be further optimized with pagination, buffer zones, binary search for the start index, and scroll throttling. Many open‑source libraries such as vue-virtual-scroller already provide robust implementations.

In the Hello Goods Mall H5 brand‑sale page, we integrated vue-virtual-scroller with pull‑to‑refresh pagination, achieving smooth infinite scrolling as shown in the GIF below.

Vuefrontend performancedynamic heightVirtual Listscroll optimizationinfinite scrolling
HelloTech
Written by

HelloTech

Official Hello technology account, sharing tech insights and developments.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.