你是否遇到过列表一多,页面就卡得像"老年机"?别怕,虚拟列表来拯救你!本文不仅带你搞懂为什么需要虚拟列表,还手把手教你如何用 React 打造高性能虚拟列表,代码、原理、细节全都有,轻松避坑!
一、为什么需要虚拟列表?
当你需要渲染成千上万条数据时,直接渲染所有 DOM 节点会让浏览器"罢工",页面卡顿、内存飙升、用户体验暴跌。
- 性能瓶颈:DOM 多了,浏览器渲染压力大。
- 内存消耗:每个节点都要占内存,数据越多越吃紧。
- 用户体验差:滚动卡顿,交互延迟,分分钟劝退用户。
所以,虚拟列表就是前端的"省电模式",只渲染你能看到的那一小部分,其他的都假装"在场",让页面飞起来!🚀
二、虚拟列表的实现原理
虚拟列表的核心思想很简单:只渲染可视区域的内容,其他用占位元素撑起高度。
1. 数据分片
- 用
slice
截取当前可见的数据片段。 - 只渲染这部分,其他的都用一个"假容器"撑起来。
2. 占位容器
- 用一个高度等于总数据长度的
div
,让滚动条看起来像真的。 - 真实渲染的列表通过
transform: translateY
精准定位到可视区。
3. 滚动监听
- 监听滚动事件,计算当前滚动到第几个元素。
- 动态更新可见数据的起止索引和偏移量。
4. 性能优化
- 只更新必要的部分,避免无意义的重渲染。
- 利用
useRef
、useState
精准控制状态。
三、我是如何打造虚拟列表的?
1. 数据准备
jsx
const listData = new Array(1000).fill(0).map((item, index) => ({ id: index, value: index }))
一千条数据,够你滑到天荒地老!
2. 计算可视区
jsx
const visibleCount = Math.ceil(500 / 50) // 可视区最多显示多少条
容器高度 500px,每条 50px,最多显示 10 条。
3. 占位容器撑高度
jsx
<div className='list-phantom' style={{height: listHeight}}></div>
撑起滚动条的"假身",让你以为真的有一千条。
4. 渲染可见数据
jsx
const visibleData = listData.slice(startIndex, endIndex)
只渲染当前可见的那几条,其他的都在"后台待命"。
5. 滚动事件驱动
jsx
const scrollEvent = () => {
const scrollTop = listRef.current.scrollTop
setStartIndex(Math.floor(scrollTop / 50))
setStartOffset(scrollTop - (scrollTop % 50))
}
每次滚动都重新计算可见区,精准定位。
6. 视觉定位
jsx
const getTransform = { transform: `translateY(${startOffset}px)` }
让真实渲染的列表"漂移"到正确的位置。
7. 样式美化
css
.container { width: 400px; height: 500px; border: 1px solid #000; overflow-y: scroll; }
ul { list-style: none; margin: 0; padding: 0; }
li { height: 50px; border: 1px solid #000; text-align: center; line-height: 50px; }
.list-phantom { position: absolute; top: 0; left: 0; right: 0; }
让虚拟列表看起来"高大上",滑动顺畅。
8. 效果展示

四、完整代码展示与注释
jsx
import React, { useEffect, useState, useRef } from 'react'
import './app.css'
// 构造一千条数据,实际场景可替换为十万条
const listData = new Array(1000).fill(0).map((item, index) => ({ id: index, value: index }))
export default function App() {
// 占位容器的偏移距离(用于定位可见区)
const [startOffset, setStartOffset] = useState(0)
// 可视区起始索引
const [startIndex, setStartIndex] = useState(0)
// 可视区结束索引
const [endIndex, setEndIndex] = useState(0)
// 列表容器引用
const listRef = useRef(null)
// 每项高度,和容器高度(可根据实际调整)
const itemHeight = 50
const containerHeight = 500
// 总列表高度
const listHeight = listData.length * itemHeight
// 可视区最多展示的项数
const visibleCount = Math.ceil(containerHeight / itemHeight)
// 每次起始索引变化,自动更新结束索引
useEffect(() => {
setEndIndex(startIndex + visibleCount)
}, [startIndex])
// 计算可视区偏移量,决定列表定位
const getTransform = {
transform: `translateY(${startOffset}px)`
}
// 截取当前可视区的数据
const visibleData = listData.slice(startIndex, endIndex)
// 滚动事件:动态计算起始索引和偏移量
const scrollEvent = () => {
const scrollTop = listRef.current.scrollTop // 容器已滚动的距离
setStartIndex(Math.floor(scrollTop / itemHeight)) // 计算新的起始索引
setStartOffset(scrollTop - (scrollTop % itemHeight)) // 计算新的偏移量
}
return (
<div className='container' onScroll={scrollEvent} ref={listRef}>
{/* 占位容器,撑起完整高度,保证滚动条正常 */}
<div className='list-phantom' style={{height: listHeight}}></div>
{/* 实际渲染的可视区列表,定位到正确位置 */}
<div className='list' style={getTransform}>
<ul>
{visibleData.map(item => (
<li key={item.id}>{item.value}</li>
))}
</ul>
</div>
</div>
)
}
五、技术亮点与优化建议
- 极致性能:只渲染可见区,页面再多数据都不卡。
- 易扩展:支持任意高度、任意数据量,轻松适配各种场景。
- 代码简洁:核心逻辑一目了然,维护无压力。
- 可玩性强:可以加动画、懒加载、无限滚动等高级玩法。
虚拟列表就像前端的"魔术师",让你在有限的空间里装下无限的数据!🎩✨
六、应用场景
- 聊天列表、评论区、商品列表、日志展示......只要数据多,都能用虚拟列表!
- 移动端、PC端通吃,性能飞升,用户体验爆表。
七、原理总结(readme.md)
markdown
# 如何一次性渲染十万条数据
1. 根据当前可视区域的高度 计算截取的起始索引 (startIndex)
2. 根据当前可视区域的高度 计算截取的结束索引 (endIndex)
3. 截取数据数组(起始索引, 结束索引),获取当前可视区域的数据 (visibleData)
4. 渲染数据
5. 每次滚动出去一个列表项后, 因为展示的值会更新, 所以要将滚出去的列表向下移动回来 startOffset
6. 为了列表能正常滚动, 用一个空容器占位, 高度设为完整列表的高度
八、总结
虚拟列表是前端性能优化的"神器",只渲染你能看到的部分,其他的都用"障眼法"撑起来。本文不仅讲清了原理,还用代码和幽默语言带你实战打造,助你轻松掌握虚拟列表的精髓。下次遇到大数据量列表,再也不用怕页面卡顿啦!😎