前端虚拟列表深度拆解

虚拟列表是为了解决什么问题

真实项目中的痛点:

想象一个后台系统:用户列表:10 万条;订单列表:20 万条;日志列表:百万级;表格里还有:多列、复杂 DOM、hover、操作按钮、状态标签

直接 map 渲染:

data.map(item => <Row key={item.id} />)

会遇到:首次渲染卡死、滚动严重掉帧、内存暴涨和浏览器直接崩

根因只有一个:DOM 太多浏览器不是怕 JS,浏览器最怕的是成千上万个 DOM 节点

总的来说虚拟列表就是只渲染可视区域内的列表项,而其余的用占位高度“假装存在”

虚拟列表的核心思想

我总结主要要理解这四点:
1.可视区域(viewport):屏幕当前能看到的那一段高度

2.列表总高度(total height):假设所有 item 都渲染后的总高度(假的,但要算出来)

3.起始索引和结束索引:根据滚动距离,计算现在应该显示哪几条

4. 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置

虚拟列表的本质实现原理

假设每一项高度固定,这个是最简单的,真是项目中大量列表数据一般都是每一项数据都是高度固定的

itemHeight = 50px 容器高度 = 500px

那屏幕最多能显示:500 / 50 = 10 条

通常会多渲染几条(缓冲区):实际渲染 = 10 + 4 = 14 条

根据滚动距离算索引

startIndex = Math.floor(scrollTop / itemHeight) endIndex = startIndex + visibleCount

不渲染所有 DOM,但要让滚动条是对的

<div> <div></div> <!-- 撑开高度 --> <div></div> <!-- 只放可见项 --> </div>
.phantom { height: totalCount * itemHeight; }

偏移当前渲染区域

offsetY = startIndex * itemHeight
.list { transform: translateY(offsetY); }

视觉效果:DOM 只有十几条、滚动条像真的有十万条

真实项目React中使用demo整体代码(可直接使用)

import React, { useRef, useState, useEffect, useMemo,useCallback } from 'react' interface VirtualListProps<T> { data: T[] height: number // 容器高度 itemHeight: number // 每一项高度(固定) renderItem: (item: T, index: number) => React.ReactNode buffer?: number // 缓冲区 } export function VirtualList<T>({ data, height, itemHeight, renderItem, buffer = 5, }: VirtualListProps<T>) { const containerRef = useRef<HTMLDivElement>(null) const [scrollTop, setScrollTop] = useState(0) /** 可视区能显示的条数 */ const visibleCount = Math.ceil(height / itemHeight) /** 开始索引 */ const startIndex = Math.max( Math.floor(scrollTop / itemHeight) - buffer, 0 ) /** 结束索引 */ const endIndex = Math.min( startIndex + visibleCount + buffer * 2, data.length ) /** 当前渲染的数据 */ const visibleData = useMemo( () => data.slice(startIndex, endIndex), [data, startIndex, endIndex] ) /** 偏移量 */ const offsetY = startIndex * itemHeight /** 总高度(关键:撑开滚动条) */ const totalHeight = data.length * itemHeight /** 滚动事件 */ const onScroll = useCallback(() => { if (!containerRef.current) return setScrollTop(containerRef.current.scrollTop) }, []) return ( <div ref={containerRef} style={{ height, overflowY: 'auto', position: 'relative', border: '1px solid #ddd', }} onScroll={onScroll} > {/* 撑开高度 */} <div style={{ height: totalHeight }} /> {/* 实际渲染内容 */} <div style={{ position: 'absolute', top: 0, left: 0, right: 0, transform: `translateY(${offsetY}px)`, }} > {visibleData.map((item, index) => ( <div key={startIndex + index} style={{ height: itemHeight, boxSizing: 'border-box', borderBottom: '1px solid #eee', }} > {renderItem(item, startIndex + index)} </div> ))} </div> </div> ) }

如何使用这个 VirtualList

const data = Array.from({ length: 100000 }, (_, i) => `Row ${i}`) export default function App() { return ( <VirtualList data={data} height={500} itemHeight={50} renderItem={(item) => <div>{item}</div>} /> ) }

针对这个demo,我总结出几个关键点

1.下面这行代码,不显示任何内容,只是为了撑开滚动条

<div style={{ height: totalHeight }} />

2.为什么 content 要 absolute + translateY?因为transform不会出发布局重排,性能比设置top好

transform: translateY(offsetY)

3.buffer 的意义,是防止滚动过快出现白屏,提前渲染上下几条

buffer = 5

4.为什么 key 用startIndex + index因为:同一条数据在不同 scrollTop 下会复用 DOM,key 必须全局唯一

可以优化的地方:可以对scroll进行一个节流处理

用 ref 记录 RAF 状态

const rafIdRef = useRef<number | null>(null)

改变 onScroll函数

const onScroll = useCallback(() => { if (!containerRef.current) return // 如果当前帧已经有任务了,直接 return if (rafIdRef.current !== null) return rafIdRef.current = requestAnimationFrame(() => { setScrollTop(containerRef.current!.scrollTop) rafIdRef.current = null }) }, [])

Read more

WIN11必备!QTTabBar中文优化版保姆级安装教程(含常见问题解决)

WIN11效率革命:深度定制你的资源管理器,不止于多标签 如果你和我一样,每天要在Windows的资源管理器里花费大量时间,那你一定对那种反复在层层文件夹中穿梭、找不到上一个窗口的体验深恶痛绝。系统自带的文件管理工具,就像一个功能简陋的毛坯房,勉强能用,但毫无效率与舒适度可言。尤其是升级到WIN11后,虽然界面更现代,但核心的文件管理逻辑依然停留在上个时代,对于追求效率的用户来说,这无疑是一种巨大的生产力损耗。 这篇文章,就是为那些不愿忍受现状,但又不想投入过多精力去学习复杂新软件的WIN10/WIN11用户准备的。我们不讨论那些需要彻底改变操作习惯的“重型”第三方管理器,而是聚焦于一种更优雅、更无感的解决方案:增强你正在使用的资源管理器本身。今天的主角,是一个经过国内开发者精心“魔改”的经典工具——QTTabBar的中文优化版。它就像给你的文件管理器做了一次精装修,保留了熟悉的格局,却赋予了它全新的、高效的能力。接下来,我将带你从零开始,完成这次效率升级,并深入探讨如何根据你的习惯,将它调校成最趁手的工具。 1. 为什么选择增强,而非替换? 在深入安装细节之前,我们有必要先

Java Web HTML问卷调查系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】

Java Web HTML问卷调查系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】

摘要 随着互联网技术的快速发展,在线问卷调查系统已成为企业、教育机构和政府部门收集数据的重要工具。传统的纸质问卷调查方式效率低下,数据统计和分析过程繁琐,而基于Web的问卷调查系统能够实现问卷的快速创建、分发和数据分析,显著提升工作效率。此外,现代用户对系统的交互体验和响应速度提出了更高要求,因此开发一个高效、稳定且用户友好的在线问卷调查系统具有重要的现实意义。关键词:问卷调查系统、Web应用、数据收集、效率提升、用户交互。 本系统采用前后端分离架构,后端基于SpringBoot2框架搭建,结合MyBatis-Plus实现高效数据库操作,MySQL8.0作为数据存储方案,确保系统的高性能和可扩展性。前端使用Vue3框架开发,利用其响应式特性和组件化设计提升用户体验。系统核心功能包括问卷创建、问题管理、用户权限控制、数据统计与可视化分析等,同时支持多终端适配,满足不同场景下的使用需求。关键词:SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0、前后端分离、数据可视化。 数据表设计 问卷信息数据表 问卷信息数据表用于存储用户创建的问卷基本信息,包括标题、

Python爬虫实战:高效解析Web of Science文献数据并导出CSV

1. 从零开始:为什么科研人员需要掌握Python爬虫 如果你是一名研究生、博士生,或者正在从事学术研究,我猜你一定有过这样的经历:为了写一篇综述或者做文献计量分析,你需要手动从Web of Science(WoS)上,一篇一篇地复制粘贴文献的标题、作者、摘要、关键词、发表年份、期刊信息……这个过程不仅枯燥乏味,而且极其容易出错,复制到第50篇的时候,你可能已经头晕眼花,甚至怀疑人生了。我当年读博的时候,为了分析一个领域近十年的研究趋势,需要收集上千篇文献数据,手动操作几乎是不可能完成的任务。正是这种“痛点”,让我下定决心研究如何用技术解放双手。 Python爬虫,听起来像是程序员专属的高深技术,但其实它离我们科研人员并不遥远。简单来说,爬虫就是一个能自动访问网页、抓取并整理信息的程序。对于Web of Science这样的学术数据库,虽然它提供了强大的检索功能,但批量导出详细数据(尤其是摘要、作者机构等)到本地进行深度分析,往往需要付费或者功能受限。自己写一个爬虫,就成了最高效、最灵活的解决方案。它能让你在喝杯咖啡的功夫,

LangChain 实战:大模型对话记忆模块(附完整代码 + Web 案例)

目录 前言:为什么需要对话记忆? 一、核心认知:原始 API vs LangChain 封装 1.1 原生 API 调用的痛点(无记忆) 1.2 LangChain 的价值:封装记忆与简化调用 二、LangChain 记忆模块核心组件 2.1 基础款:ConversationBufferMemory(完整记忆) 2.2 进阶款:窗口记忆与总结记忆 (1)ConversationBufferWindowMemory(窗口记忆) (2)ConversationSummaryMemory(总结记忆) 三、实战 1:LangChain 记忆链(ConversationChain) 四、实战 2:Streamlit 搭建带记忆的聊天