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.
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.
HelloTech
Official Hello technology account, sharing tech insights and developments.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
