刷到一个高频面试题:在页面内一次性渲染 10 万条记录,怎么保证页面不卡顿、滚动不卡顿?
我就是要把这 10 万条数据直接通过 v-for 渲染出来,会发生什么?
<script setup lang="ts">
import { computed } from "vue";
type Row = { id: number; text: string };
const list = computed<Row[]>(() => {
// 模拟 10 万条
return Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
text: `第 ${i + 1} 条记录`
}));
});
</script>
<template>
<div style="height: 600px; overflow: auto; border: 1px solid #ddd;">
<div v-for="item in list" :key="item.id" style="padding: 8px 12px; border-bottom: 1px solid #eee;">
{{ item.text }}
</div>
</div>
</template>
会发现:
渲染 10 万条的真正成本在于:创建并维护 10 万个 DOM 节点(以及它们的样式、布局、绘制)。
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
type Row = { id: number; text: string };
// 模拟 10 万条数据
const data = ref<Row[]>(
Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
text: `第 ${i + 1} 条记录`
}))
);
const containerRef = ref<HTMLElement | null>(null);
const scrollTop = ref(0);
const itemHeight = 36; // 每行固定高度
const buffer = 6; // 上下缓冲,避免快速滚动白屏
const containerHeight = ref(600); // 容器高度(也可动态测量)
onMounted(() => {
// 可选:动态拿容器高度(更通用)
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight;
}
});
const totalHeight = computed(() => data.value.length * itemHeight);
const visibleCount = computed(() =>
Math.ceil(containerHeight.value / itemHeight)
);
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer)
);
const endIndex = computed(() =>
Math.min(
data.value.length,
startIndex.value + visibleCount.value + buffer * 2
)
);
const visibleList = computed(() =>
data.value.slice(startIndex.value, endIndex.value)
);
const offsetY = computed(() => startIndex.value * itemHeight);
function onScroll(e: Event) {
scrollTop.value = (e.target as HTMLElement).scrollTop;
}
</script>
<template>
<div
ref="containerRef"
@scroll="onScroll"
style="height: 600px; overflow: auto; border: 1px solid #ddd;"
>
<!-- 1) 占位:撑起总高度,让滚动条看起来是 10 万条 -->
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<!-- 2) 实际渲染层:只渲染可视区 -->
<div
:style="{
position: 'absolute',
left: 0,
right: 0,
transform: `translateY(${offsetY}px)`
}"
>
<div
v-for="item in visibleList"
:key="item.id"
:style="{
height: itemHeight + 'px',
lineHeight: itemHeight + 'px',
padding: '0 12px',
borderBottom: '1px solid #eee',
boxSizing: 'border-box'
}"
>
{{ item.text }}
</div>
</div>
</div>
</div>
<div style="margin-top: 8px; color: #666; font-size: 12px;">
渲染区间:{{ startIndex }} ~ {{ endIndex }}(实际渲染 {{ endIndex - startIndex }} 条)
</div>
</template>
当然也有现成的库可用,vue3可用库:vue3-virtual-scroller 或组件库自带 VirtualList
虚拟列表 DOM 数量就维持在几十条,保证可视区域和缓冲区的数据即可。滚动的时候,替换可视区数据,DOM 数量不变。
即:虚拟列表不是“让 10 万条渲染更快”,而是让页面永远只渲染你看得见的那一小部分。