📊前端虚拟列表(Virtual List)从原理到实战:海量数据渲染终极方案
🚀 一、虚拟列表是什么?为什么要用?
1. 痛点:长列表渲染性能灾难
在前端业务中,我们经常遇到海量数据列表场景:聊天记录、日志列表、用户列表、商品列表、表格数据等。
传统做法是:一次性把所有 DOM 渲染到页面。
- 数据量 100 条以内:流畅无压力;
- 数据量 1000+:明显卡顿、白屏、滚动掉帧;
- 数据量 10000+:页面直接卡死,甚至崩溃。
根本原因:DOM 数量过多 → 重排重绘频繁 → 主线程阻塞。
2. 虚拟列表(Virtual List)核心思想
只渲染可视区域内的 DOM,可视区域外的 DOM 全部不渲染或销毁,通过滚动位置动态计算当前应该显示哪些数据。
一句话总结:
用少量 DOM(几十条)模拟海量列表(几万、几十万条)。
3. 虚拟列表核心优势
- 无论数据量 1000 还是 100000,渲染 DOM 数量始终固定(可视区能容纳几条就几条);
- 滚动极流畅,无白屏、无卡顿;
- 内存占用极低,大幅降低页面崩溃风险;
- 兼容 PC、H5、小程序、Electron 等所有前端环境。
🎯 二、虚拟列表核心原理(必懂)
1. 三个关键区域
- 可视区域(Viewport):用户能看到的列表区域;
- 缓冲区域(Buffer):可视区上下额外多渲染几条,防止快速滚动时出现白屏;
- 真实列表总高度:用一个空 div 撑起高度,模拟完整列表长度,让滚动条行为正常。
2. 核心计算步骤
- 获取可视区域高度;
- 确定每一行高度(固定高度 / 动态高度);
- 根据滚动距离 scrollTop 计算:
- 起始索引 startIndex
- 结束索引 endIndex
- 截取数据:
list.slice(startIndex, endIndex); - 渲染截取后的少量数据;
- 通过
paddingTop或transform模拟列表滚动偏移,让内容出现在正确位置。
3. 两种虚拟列表类型
| 类型 | 特点 | 适用场景 | 实现难度 |
|---|---|---|---|
| 固定高度虚拟列表 | 每行高度相同 | 普通列表、表格、日志 | 简单 |
| 动态高度虚拟列表 | 每行高度不确定 | 聊天、评论、富文本、卡片 | 复杂 |
本文先讲最常用、最稳定、工程首选:固定高度虚拟列表,再简单扩展动态高度思路。
📁三、原生 JS 实现固定高度虚拟列表(可直接复制)
实现步骤
- 容器设置
overflow: auto; - 内置一个占位 div 撑起总高度;
- 真实列表区域用
paddingTop偏移; - 监听滚动事件,动态计算显示区间;
- 只渲染可视区 + 缓冲区数据。
完整代码(原生 JS)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生JS虚拟列表</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.virtual-list {
width: 600px;
height: 600px; /* 可视区域高度 */
border: 1px solid #eee;
overflow-y: auto; /* 关键:滚动容器 */
position: relative;
}
.list-sentinel {
height: calc(var(--total-height) * 1px); /* 撑起总高度 */
}
.list-body {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding-top: calc(var(--offset-y) * 1px); /* 偏移量 */
}
.item {
height: 60px; /* 固定行高 */
padding: 0 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
</style>
</head>
<body>
<div class="virtual-list" id="virtualList">
<div class="list-sentinel" id="sentinel"></div>
<div class="list-body" id="listBody"></div>
</div>
<script>
// 1. 模拟海量数据(10万条)
const totalData = Array.from({ length: 100000 }, (_, i) => ({
id: i,
content: `虚拟列表第 ${i} 条数据`
}))
// 2. 配置
const ITEM_HEIGHT = 60 // 行高
const VIEW_HEIGHT = 600 // 可视高度
const BUFFER_COUNT = 10 // 上下缓冲区条数
const listEl = document.getElementById('virtualList')
const sentinelEl = document.getElementById('sentinel')
const listBodyEl = document.getElementById('listBody')
// 3. 设置总高度(CSS变量)
sentinelEl.style.setProperty('--total-height', totalData.length * ITEM_HEIGHT)
// 4. 滚动监听
listEl.addEventListener('scroll', () => {
renderVirtualList()
})
// 5. 核心渲染函数
function renderVirtualList() {
const scrollTop = listEl.scrollTop
// 计算起始索引
const startIndex = Math.floor(scrollTop / ITEM_HEIGHT)
// 可视区能容纳条数
const viewCount = Math.ceil(VIEW_HEIGHT / ITEM_HEIGHT)
// 结束索引(加缓冲区)
const endIndex = startIndex + viewCount + BUFFER_COUNT
// 真实起始(往前缓冲)
const realStart = Math.max(0, startIndex - BUFFER_COUNT)
const realEnd = Math.min(totalData.length, endIndex)
// 截取数据
const showList = totalData.slice(realStart, realEnd)
// 偏移量 = realStart * 行高
listBodyEl.style.setProperty('--offset-y', realStart * ITEM_HEIGHT)
// 渲染
listBodyEl.innerHTML = showList.map(item => `
<div class="item">${item.content}</div>
`).join('')
}
// 初始渲染
renderVirtualList()
</script>
</body>
</html>
代码关键点解析
overflow-y: auto:滚动容器;list-sentinel:用总高度撑开滚动条,行为和真实长列表一致;padding-top: realStart * ITEM_HEIGHT:把列表内容"推"到正确位置;BUFFER_COUNT:上下缓冲,避免快速滚动白屏;- 只渲染
realStart ~ realEnd之间的数据,通常 20~30 条 DOM 搞定 10w 数据。
⭐ 四、Vue3 + TS 实现企业级虚拟列表(推荐)
实际项目中,我们一般封装为可复用组件。下面给出 Vue3 + TypeScript + 固定高度 标准虚拟列表组件,可直接用于生产。
1. 组件:VirtualList.vue
vue
<template>
<div
ref="listRef"
class="virtual-list"
@scroll="handleScroll"
style="height: 600px"
>
<!-- 占位撑高 -->
<div class="sentinel" :style="{ height: totalHeight + 'px' }"></div>
<!-- 真实列表 -->
<div class="list-body" :style="{ paddingTop: offsetY + 'px' }">
<div v-for="item in showList" :key="item.id" class="item">
{{ item.content }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
interface ListItem {
id: number
content: string
}
// 外部传入数据
const props = defineProps<{
list: ListItem[]
itemHeight?: number
viewHeight?: number
bufferCount?: number
}>()
const itemHeight = props.itemHeight || 60
const viewHeight = props.viewHeight || 600
const bufferCount = props.bufferCount || 10
const listRef = ref<HTMLDivElement | null>(null)
// 滚动距离
const scrollTop = ref(0)
// 总高度
const totalHeight = computed(() => props.list.length * itemHeight)
// 计算显示区间
const renderRange = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight)
const viewCount = Math.ceil(viewHeight / itemHeight)
const end = start + viewCount + bufferCount
const realStart = Math.max(0, start - bufferCount)
const realEnd = Math.min(props.list.length, end)
return { realStart, realEnd }
})
// 偏移量
const offsetY = computed(() => renderRange.value.realStart * itemHeight)
// 最终显示列表
const showList = computed(() => {
const { realStart, realEnd } = renderRange.value
return props.list.slice(realStart, realEnd)
})
// 滚动事件
function handleScroll() {
if (listRef.value) {
scrollTop.value = listRef.value.scrollTop
}
}
// 初始化
onMounted(() => {
handleScroll()
})
// 数据变化重新计算
watch(() => props.list, () => {
handleScroll()
})
</script>
<style scoped>
.virtual-list {
width: 100%;
overflow-y: auto;
position: relative;
border: 1px solid #eee;
}
.sentinel {
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
}
.list-body {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.item {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
border-bottom: 1px solid #f5f5f5;
}
</style>
2. 使用组件
vue
<template>
<VirtualList :list="totalData" :item-height="60" :view-height="600" />
</template>
<script setup lang="ts">
import VirtualList from './VirtualList.vue'
// 模拟10万条数据
const totalData = Array.from({ length: 100000 }, (_, i) => ({
id: i,
content: `虚拟列表第 ${i} 行内容`
}))
</script>
特点:
- 完全响应式、TS 类型安全;
- 支持自定义行高、可视高度、缓冲区;
- 无第三方依赖,性能极致;
- 可直接用于后台管理系统、大数据列表、日志面板。
🔧五、动态高度虚拟列表(思路 + 实现要点)
如果你的列表高度不固定(文本长短不一、图片、富文本),固定高度方案会失效。
核心思路
- 先预估高度渲染,避免白屏;
- 元素渲染后,获取真实 DOM 高度;
- 维护一个 position 数组 ,记录每一项的
top、bottom、height; - 滚动时通过二分查找快速定位 startIndex;
- 实时更新位置信息。
关键技术点
- 使用
ResizeObserver监听元素高度变化; - 二分查找替代遍历,大幅提升性能;
- position 数组缓存所有行位置,滚动时 O(logN) 定位。
由于动态高度实现较复杂、代码量大,且大多数后台系统可用固定高度 + 换行省略替代,本文不再贴完整代码,需要我可以单独开一篇讲动态高度虚拟列表。
🔍六、虚拟列表性能优化要点
1. 合理设置缓冲区
- 缓冲区太小:快速滚动容易白屏;
- 缓冲区太大:DOM 变多,性能下降;
- 推荐:上下各缓冲 5~15 条。
2. 避免滚动事件频繁触发(防抖可选)
虚拟列表本身计算极轻量,一般不需要防抖。
极端复杂列表可加:
ts
import { debounce } from 'lodash-es'
const handleScroll = debounce(() => { ... }, 10)
3. 列表项使用 CSS containment
强制独立渲染层,减少重排范围:
css
.item {
contain: layout paint size;
}
4. 列表项 key 必须稳定且唯一
不要用 index 作为 key,否则滚动复用 DOM 时会出现内容错乱、闪烁。
5. 大数据列表使用 v-show 而非 v-if(Vue)
虚拟列表适合复用 DOM ,配合 track-by / key 性能最优。
🙅♂️七、虚拟列表适用 & 不适用场景
适用场景
- 后台系统表格、日志、操作记录;
- 聊天列表、评论列表;
- 大数据看板、埋点日志、海量商品;
- 滚动加载无限列表(无限滚动 + 虚拟列表)。
不适用场景
- 数据量 < 50 条(直接渲染更简单);
- 极复杂、嵌套极深、大量图片/视频/Canvas 混合;
- 需要完整 DOM 结构才能正常工作的第三方库(如某些复杂表格插件)。
🔥 八、成熟开源虚拟列表库(生产推荐)
如果你不想自己造轮子,直接用以下成熟库:
- vue-virtual-scroller(Vue 生态最稳)
- vue3-virtual-list(轻量、Vue3 专用)
- react-virtualized / react-window(React 主流)
- antd-virtual-list 、element-plus 内置虚拟列表(组件库自带)
- better-scroll(支持移动端、下拉刷新 + 虚拟列表)
📌 九、总结
- 虚拟列表核心:只渲染可视区 + 缓冲区 DOM,用少量 DOM 模拟长列表;
- 固定高度虚拟列表:实现简单、性能稳定、满足 90% 业务;
- 关键公式:
startIndex = Math.floor(scrollTop / itemHeight)offsetY = realStart * itemHeighttotalHeight = list.length * itemHeight
- 滚动时只做计算 + 截取数据,不操作大量 DOM,性能极致;
- 企业级开发建议:自己封装基础版 + 复杂场景用成熟库。
虚拟列表是前端性能优化必考、必掌握的核心方案,学会它,任何长列表、大数据量场景都不再有性能压力。