前端虚拟列表深度拆解

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

真实项目中的痛点:

想象一个后台系统:用户列表: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

2025年第十六届蓝桥杯网络安全CTF省赛(初赛)真题详解Writeup(Web、Misc、Crypto、Reverse、Pwn)

2025年第十六届蓝桥杯网络安全CTF省赛(初赛)真题详解Writeup(Web、Misc、Crypto、Reverse、Pwn)

今年是第三届蓝桥杯网络安全CTF竞赛,相比于前两届,今年没有了理论题。这三年题目难度呈逐年上升趋势,以后大概率会越来越难。 第一题:情报收集:黑客密室逃脱(Web类题目50分) 1.1 题目描述: 靶机题目:黑客密室逃脱 题目内容:欢迎闯入黑客密室,你被困在了顶级黑客精心设计的数字牢笼中,每一道关卡都暗藏致命陷阱!唯一的逃脱之路,是破解散落在服务器各处的加密线索,找到最终的“数字钥匙”。赛题原题及工具资料下载地址:www.whsjyc.cn 访问靶机后如下图所示: 点击【立即查看日志】: 给出一串加密字符串,这就是密文,解密之后就是flag,点击【前往秘密区域】: 提示我们去访问/file?name=xxx,让我们猜测文件名。我们通过课程教授的方法获取到文件名是app.py,然后通过文件包含获取源代码: import os from flask import Flask, request, render_

SpringBoot+Vue 物流管理系统平台完整项目源码+SQL脚本+接口文档【Java Web毕设】

SpringBoot+Vue 物流管理系统平台完整项目源码+SQL脚本+接口文档【Java Web毕设】

摘要 随着电子商务和物流行业的快速发展,高效的物流管理系统成为企业提升竞争力的关键。传统物流管理方式依赖人工操作,效率低下且容易出错,难以满足现代物流需求。物流管理系统通过信息化手段整合订单、运输、仓储等环节,实现物流全流程的可视化和智能化管理。该系统能够优化资源配置,降低运营成本,提高配送效率,为用户提供更优质的服务体验。关键词:物流管理、电子商务、信息化、智能化、资源配置。 本系统采用SpringBoot和Vue.js技术栈开发,前后端分离架构提升了系统的可维护性和扩展性。后端使用SpringBoot框架实现RESTful API,提供订单管理、运输跟踪、仓储管理等功能;前端基于Vue.js构建用户界面,实现数据可视化与交互操作。数据库采用MySQL存储物流数据,通过SQL脚本实现数据表的初始化与维护。系统还提供完整的接口文档,便于二次开发和集成。关键词:SpringBoot、Vue.js、RESTful API、MySQL、接口文档。 数据表 物流订单数据表 订单数据表用于存储用户提交的物流订单信息,包括订单状态、收发件人信息等。订单编号是该表的主键,创建时间通过函数自

Web 创建设计

下面为你整理一份系统全面、通俗易懂、适合初学者与进阶者的 《Web 创建与设计指南》(Web Creation & Design Guide)。 它覆盖从网站构思、设计、前端、后端、交互、发布到维护的完整流程。 如果你愿意,我还可以将它扩展成 PDF、PPT、Markdown 或课程体系。 🌐 Web 创建与设计指南 (Web Creation & Design Guide) 1. 什么是 Web 创建与设计? Web 创建(Web Development)= 网站功能的开发(HTML/CSS/JS + 后端 + 数据库) Web 设计(Web Design)= 网站视觉与体验设计(UI/UX)

【前端面经】字节前端社招面经分享(已offer)

社招时间线 全程面试时间都是候选人定的,字节效率还是非常高的 * 10.23 HR电话沟通约面 * 10.28 技术一面(两小时后告知通过约面) * 10.30 技术二面(半小时后告知通过约面) * 11.4 技术三面(两小时后告知通过约面) * 11.5 HR面(三小时后告知通过) * 11.5 OC * 11.5 收集薪资流水证明 * 11.6 谈薪 * 11.11 书面offer 面试 基本都是从简历出发深挖问题,没有太多通用性,仅列出偏技术点不涉及具体项目的问题。 因为AI相关内容较多,所以问题也偏AI。 技术一面(1h) 1. 代码输出题:闭包与变量提升相关 2. 手写题:数组转树形结构 3. 手写题: