High‑Performance GridList Component with Pagination and Virtual Scrolling in Vue 3
This article introduces a Vue 3 GridList component that supports responsive grid layouts, server‑side pagination via a request function, and virtual scrolling to render only visible items, providing performance improvements for large data sets while offering clear usage examples and full source code.
Lists are a common UI component; this article shares a comfortable implementation of a high‑performance GridList component for Vue 3, including usage examples, implementation ideas, pagination handling, and virtual scrolling optimization.
Usage and effect
Below is a grid list demo (image omitted) followed by the component usage code.
<script setup lang="ts">
import GridList, { RequestFunc } from '@/components/GridList.vue';
const data: RequestFunc
= ({ page, limit }) => {
return new Promise((resolve) => {
console.log('开始加载啦', page, limit);
setTimeout(() => {
resolve({
data: Array.from({ length: limit }, (_, index) => index + (page - 1) * limit),
total: 500,
});
}, 1000);
});
};
</script>
<template>
<GridList :request="data" :column-gap="20" :row-gap="20" :limit="100" :item-min-width="200" class="grid-list">
<template #empty><p>暂无数据</p></template>
<template #default="{ item }"><div class="item">{{ item }}</div></template>
<template #loading><p>加载中...</p></template>
<template #noMore><p>没有更多了</p></template>
</GridList>
</template>The row‑list variant is achieved by setting item-min-width="100%" , making each item fill the container width. The corresponding code is similar, only the :item-min-width prop differs.
<script setup lang="ts">
import GridList, { RequestFunc } from '@/components/GridList.vue';
const data: RequestFunc
= ({ page, limit }) => {
return new Promise((resolve) => {
console.log('开始加载啦', page, limit);
setTimeout(() => {
resolve({
data: Array.from({ length: limit }, (_, index) => index + (page - 1) * limit),
total: 500,
});
}, 1000);
});
};
</script>
<template>
<GridList :request="data" :column-gap="20" :row-gap="20" :limit="100" :item-min-width="'100%'" class="grid-list">
...
</GridList>
</template>Implementation ideas – grid layout
The GridList component receives data via a request function (type RequestFunc ) and uses CSS Grid to create a responsive layout. Important props include item-min-width , item-min-height , row-gap , and column-gap . The component’s script defines these props and sets up a reactive data array.
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
const props = defineProps<{
dataSource?: any[];
itemMinWidth?: number;
itemMinHeight?: number;
rowGap?: number;
columnGap?: number;
}>();
const data = ref<any[]>([...props.dataSource]);
</script>
<template>
<div ref="containerRef" class="infinite-list-wrapper">
<div v-else class="list">
<div v-for="(item, index) in data" :key="index">
<slot :item="item" :index="index">{{ item }}</slot>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.infinite-list-wrapper {
text-align: center;
overflow-y: scroll;
position: relative;
-webkit-overflow-scrolling: touch;
.list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(calc(v-bind(itemMinWidth) * 1px), 1fr));
grid-auto-rows: minmax(auto, calc(v-bind(itemMinHeight) * 1px));
column-gap: calc(v-bind(columnGap) * 1px);
row-gap: calc(v-bind(rowGap) * 1px);
div:first-of-type { grid-column-start: 1; grid-column-end: 1; }
}
}
</style>The component also supports pagination. Instead of passing a static data-source , a request prop of type RequestFunc is provided. The function receives a Pagination object ({page, limit}) and returns a Promise of { data, total } . The component calls this function when the scroll reaches the bottom, automatically handling page increments and loading states.
export interface Pagination { limit: number; page: number; }
export interface RequestResult
{ data: T[]; total: number; }
export type RequestFunc
= (pagination: Pagination) => Promise
> | RequestResult
;Inside the component, loading , total , page , and a computed noMore flag manage the pagination flow. When the user scrolls near the bottom, props.request is invoked, the new data is appended, and page is incremented if more data remains.
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
const props = defineProps<{ request?: RequestFunc
; limit?: number; loadDistance?: number; }>();
const containerRef = ref<HTMLDivElement>();
const loading = ref(false);
const data = ref<any[]>([]);
const total = ref(0);
const page = ref(1);
const noMore = computed(() => total.value === 0 || data.value.length >= total.value || data.value.length < props.limit);
async function load() {
loading.value = true;
const result = await Promise.resolve(props.request({ limit: props.limit, page: page.value }));
total.value = result.total;
data.value.push(...result.data);
if (!noMore.value) page.value += 1;
loading.value = false;
}
</script>Virtual list
To further improve performance for massive data sets, a virtual‑grid‑list hook useVirtualGridList is introduced. It calculates visible rows, column numbers, start/end indices, and a placeholder element ( phantomElement ) whose height matches the total list height, ensuring the scrollbar reflects the full content while only rendering items within the viewport.
export const useVirtualGridList = ({ containerRef, itemMinWidth, itemMinHeight, rowGap, columnGap, data }) => {
const phantomElement = document.createElement('div');
const containerHeight = ref(0);
const containerWidth = ref(0);
const startIndex = ref(0);
const endIndex = ref(0);
const startOffset = ref(0);
const columnNum = computed(() => Math.floor((containerWidth.value - itemMinWidth.value) / (itemMinWidth.value + columnGap.value)) + 1);
const rowNum = computed(() => Math.ceil(data.value.length / columnNum.value));
const listHeight = computed(() => Math.max(rowNum.value * itemMinHeight.value + (rowNum.value - 1) * rowGap.value, 0));
const visibleRowNum = computed(() => Math.ceil((containerHeight.value - itemMinHeight.value) / (itemMinHeight.value + rowGap.value)) + 1);
const visibleCount = computed(() => visibleRowNum.value * columnNum.value);
watch(() => listHeight.value, () => { phantomElement.style.height = `${listHeight.value}px`; });
watchEffect(() => { endIndex.value = startIndex.value + visibleCount.value; });
const handleScroll = () => {
const scrollTop = containerRef.value?.scrollTop ?? 0;
const startRow = Math.ceil((scrollTop - itemMinHeight.value) / (itemMinHeight.value + rowGap.value));
startIndex.value = startRow * columnNum.value;
startOffset.value = scrollTop - (scrollTop % (itemMinHeight.value + rowGap.value));
};
onMounted(() => {
if (containerRef.value) {
containerRef.value.appendChild(phantomElement);
containerRef.value.addEventListener('scroll', handleScroll);
handleScroll();
}
});
return { startIndex, endIndex, startOffset, listHeight };
};The hook is used inside GridList to render only data.slice(startIndex, endIndex) and to position the visible block with startOffset . This dramatically reduces DOM nodes and keeps scrolling smooth.
Performance demonstration
Loading 100,000 items at once shows the benefit of virtual scrolling; combining lazy loading (pagination) with virtual scrolling further improves memory usage and initial render time.
Source code is available on GitHub, and readers are encouraged to star or like the article if it helped them.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.