为什么需要虚拟列表
在实际后台系统中,我们经常遇到用户列表、订单列表甚至日志列表数据量巨大的情况。动辄十万、百万级的数据,如果直接通过 map 渲染所有 DOM,浏览器会面临首次渲染卡死、滚动严重掉帧、内存暴涨甚至崩溃的风险。
根本原因只有一个:DOM 节点太多。浏览器并不怕 JS 计算量大,它最怕的是成千上万个 DOM 节点的布局与重绘。虚拟列表的核心思想就是只渲染可视区域内的列表项,其余部分用占位高度'假装存在',从而将 DOM 数量控制在合理范围。
核心设计思路
理解虚拟列表主要需把握四个维度:
- 可视区域(viewport):屏幕当前能看到的实际高度。
- 列表总高度:假设所有 item 都渲染后的理论总高度,用于撑开滚动条。
- 起始和结束索引:根据滚动距离计算当前应该显示哪几条数据。
- 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置。
实现原理详解
假设每一项高度固定,这是最常见的场景。例如 itemHeight = 50px,容器高度 500px,那么屏幕最多能显示 10 条。为了体验流畅,通常会多渲染几条作为缓冲区,比如实际渲染 14 条。
计算逻辑很简单:
startIndex = Math.floor(scrollTop / itemHeight)
endIndex = startIndex + visibleCount
关键步骤在于不渲染所有 DOM,但要保证滚动条长度是对的。我们需要一个占位元素撑开高度,同时通过 transform 移动可见内容:
<div>
<div style="height: totalCount * itemHeight"></div> <!-- 撑开高度 -->
<div style="transform: translateY(offsetY)"></div> <!-- 只放可见项 -->
</div>
这样视觉效果就是 DOM 只有十几条,但滚动条却像真的有十万条一样顺滑。
React 组件实现
下面是一个基于 TypeScript 的通用虚拟列表组件,可直接集成到项目中。
import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react'
interface VirtualListProps<T> {
: T[]
:
:
: .
?:
}
<T>({
data,
height,
itemHeight,
renderItem,
buffer =
}: <T>) {
containerRef = useRef<>()
[scrollTop, setScrollTop] = ()
visibleCount = .(height / itemHeight)
startIndex = .(
.(scrollTop / itemHeight) - buffer,
)
endIndex = .(
startIndex + visibleCount + buffer * ,
data.
)
visibleData = (
data.(startIndex, endIndex),
[data, startIndex, endIndex]
)
offsetY = startIndex * itemHeight
totalHeight = data. * itemHeight
onScroll = ( {
(!containerRef.)
(containerRef..)
}, [])
(
)
}

