Efficient Rendering of Large Datasets in Vue.js Frontend Applications
This article demonstrates several techniques—including server‑side data simulation, batch rendering with timers, requestAnimationFrame, pagination components, infinite scroll, and virtual lists—to efficiently display and interact with 100,000 records in a Vue.js front‑end without causing UI freezes or performance degradation.
Simulating 100,000 Records
Use express to create an API endpoint that returns an array of 100,000 objects, each containing id , name , and value fields.
route.get("/bigData", (req, res) => {
res.header('Access-Control-Allow-Origin', '*'); // allow CORS
let arr = [] // store 100k items
for (let i = 0; i < 100000; i++) {
arr.push({
id: i + 1,
name: '名字' + (i + 1),
value: i + 1,
})
}
res.send({ code: 0, msg: '成功', data: arr }) // return data
})Send Request on Button Click
Bind a click handler to an el-button that triggers an asynchronous request and stores the result in the component state.
<el-button :loading="loading" @click="onClick">点击请求加载</el-button>
<el-table :data="arr">
<el-table-column type="index" label="序" />
<el-table-column prop="id" label="ID" />
<el-table-column prop="name" label="名字" />
<el-table-column prop="value" label="对应值" />
</el-table>
data() {
return {
arr: [],
loading: false,
};
},
async onClick() {
// send request, assign data to arr
}Solution 1: Direct Rendering (Not Recommended)
Fetching all data and assigning it directly to the table causes the page to freeze because the browser must render 100,000 DOM nodes at once.
async onClick() {
this.loading = true;
const res = await axios.get("http://ashuai.work:10000/bigData");
this.arr = res.data.data;
this.loading = false;
}Solution 2: Batch Rendering with setTimeout
Split the 100k array into small chunks (e.g., 10 items per chunk) and render each chunk sequentially using setTimeout , which prevents the UI from freezing.
function averageFn(arr) {
let i = 0;
let result = [];
while (i < arr.length) {
result.push(arr.slice(i, i + 10));
i = i + 10;
}
return result;
}
async onClick() {
this.loading = true;
const res = await axios.get("http://ashuai.work:10000/bigData");
this.loading = false;
let twoDArr = averageFn(res.data.data);
for (let i = 0; i < twoDArr.length; i++) {
setTimeout(() => {
this.arr = [...this.arr, ...twoDArr[i]];
}, 1000 * i); // interval ~16.6 ms (1000/60)
}
},Solution 3: Rendering with requestAnimationFrame
Use requestAnimationFrame instead of setTimeout to align rendering with the browser’s repaint cycle, achieving smoother updates.
async onClick() {
this.loading = true;
const res = await axios.get("http://ashuai.work:10000/bigData");
this.loading = false;
let twoDArr = averageFn(res.data.data);
const use2DArrItem = (page) => {
if (page > twoDArr.length - 1) return;
requestAnimationFrame(() => {
this.arr = [...this.arr, ...twoDArr[page]];
use2DArrItem(page + 1);
});
};
use2DArrItem(0);
},Solution 4: Front‑End Pagination with Element‑UI
Leverage the built‑in pagination component of Element‑UI to display a subset of rows per page.
<template>
<div class="box">
<el-table height="240" :data="showTableData" border style="width: 100%">
<el-table-column prop="name" label="姓名" width="180"></el-table-column>
<el-table-column prop="age" label="年龄" width="180"></el-table-column>
<el-table-column prop="home" label="家乡"></el-table-column>
</el-table>
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageIndex"
:page-size="pageSize"
:page-sizes="[2, 4, 6, 10]"
:total="total"
></el-pagination>
</div>
</template>
<script>
export default {
data() { return { pageIndex: 1, pageSize: 4, total: 0, allTableData: [], showTableData: [] }; },
mounted() { /* fetch all data and init pagination */ },
methods: {
getShowTableData() { let begin = (this.pageIndex - 1) * this.pageSize; let end = this.pageIndex * this.pageSize; this.showTableData = this.allTableData.slice(begin, end); },
handleCurrentChange(val) { this.pageIndex = val; this.getShowTableData(); },
handleSizeChange(val) { this.pageIndex = 1; this.pageSize = val; this.getShowTableData(); }
}
};
</script>Solution 5: Scroll‑Bottom Auto‑Load
Detect when the scroll reaches the bottom using either scrollTop + clientHeight >= innerHeight or a MutationObserver , then load the next batch of data.
scrollTop + clientHeight >= innerHeight
new MutationObserver()Solution 6: Virtual List (Windowing)
Render only the rows that are visible in the viewport and recycle DOM nodes as the user scrolls, dramatically reducing the number of elements the browser must manage.
<template>
<div class="virtualListWrap" ref="virtualListWrap" @scroll="handleScroll" :style="{ height: itemHeight * count + 'px' }">
<div class="placeholderDom" :style="{ height: allListData.length * itemHeight + 'px' }"></div>
<div class="contentList" :style="{ top: topVal }">
<div v-for="(item, index) in showListData" :key="index" class="itemClass" :style="{ height: itemHeight + 'px' }">{{ item.name }}</div>
</div>
<div class="loadingBox" v-show="loading"><i class="el-icon-loading"></i> loading...</div>
</div>
</template>
<script>
export default {
data() { return { allListData: [], itemHeight: 40, count: 10, start: 0, end: 10, topVal: '0px', loading: false }; },
computed: { showListData() { return this.allListData.slice(this.start, this.end); } },
async created() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.allListData = res.data.data; this.loading = false; },
methods: { handleScroll() { const scrollTop = this.$refs.virtualListWrap.scrollTop; this.start = Math.floor(scrollTop / this.itemHeight); this.end = this.start + this.count; this.topVal = scrollTop + 'px'; } }
};
</script>Solution 7: VXETable Virtual Scrolling
For table‑based data, the vxe-table component provides built‑in virtual scrolling that can handle up to 50,000 columns and 300,000 rows.
// main.js
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
Vue.use(VXETable)
<template>
<vxe-table border show-overflow ref="xTable1" height="300" :row-config="{ isHover: true }" :loading="loading">
<vxe-column type="seq"/>
<vxe-column field="id" title="ID"/>
<vxe-column field="name" title="名字"/>
<vxe-column field="value" title="对应值"/>
</vxe-table>
</template>
<script>
export default {
data() { return { loading: false }; },
async created() { this.loading = true; const res = await axios.get("http://ashuai.work:10000/bigData"); this.loading = false; this.$refs.xTable1.loadData(res.data.data); }
};
</script>All the above methods aim to keep the UI responsive while dealing with massive front‑end data sets, allowing developers to choose the strategy that best fits their project requirements.
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.