跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
TypeScript大前端算法

深入理解前端虚拟列表:原理与 React 实现

虚拟列表通过仅渲染可视区域元素解决大数据量下的页面卡顿问题。核心在于计算起始索引、结束索引及偏移量,配合占位高度模拟滚动条。提供基于 React 和 TypeScript 的完整实现方案,涵盖固定高度场景、缓冲区策略及节流优化技巧,适用于后台管理系统等高性能列表场景。

Ne0发布于 2026/4/9更新于 2026/5/2111 浏览

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

真实项目中的痛点:

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

目录

  1. 虚拟列表是为了解决什么问题
  2. 虚拟列表的核心思想
  3. 虚拟列表的本质实现原理
  4. React 实战:虚拟列表完整代码示例
  5. 如何使用这个 VirtualList
  6. 进阶优化:滚动事件节流
  • 💰 8折买阿里云服务器限时8折了解详情
.phantom { height: totalCount * itemHeight; }

偏移当前渲染区域

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

**视觉效果:**DOM 只有十几条,但滚动条像真的有十万条。

React 实战:虚拟列表完整代码示例

下面是一个可直接使用的 TypeScript 实现,封装了核心逻辑。

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)}
          </>
        ))}
      
    
  )
}

如何使用这个 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>}
    />
  )
}

结合这段代码,有几个关键细节需要注意:

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

    <div style={{ height: totalHeight }} />
    
  2. 性能优化:为什么 content 要用 absolute + translateY?因为 transform 不会触发布局重排,性能比设置 top 好得多。

    transform: translateY(offsetY)
    
  3. 缓冲区策略:buffer 的意义是防止滚动过快出现白屏,提前渲染上下几条数据。

    buffer = 5
    
  4. Key 的唯一性:为什么 key 用 startIndex + index?因为同一条数据在不同 scrollTop 下会复用 DOM,key 必须全局唯一,不能仅依赖 item id。

进阶优化:滚动事件节流

上面的 demo 中,onScroll 函数在高频触发时可能会影响性能。可以对 scroll 进行一个节流处理,利用 RAF(RequestAnimationFrame)来同步状态。

用 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
  })
}, [])

这样能保证每帧只更新一次状态,避免不必要的重渲染。

div
</div>
</div>
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 多平台快速部署指南(Mac/Windows/阿里云)
  • Adobe Illustrator 2025 安装步骤与使用技巧指南
  • VS Code 远程连接服务器后 GitHub Copilot 无法使用解决方案
  • Linux 管道机制与 Java finally 执行逻辑解析
  • OpenClaw 龙虾 AI 全能助手安装与配置指南
  • Web 虚拟卡销售平台实现方案
  • .NET 集成 GoView 低代码可视化大屏实战指南
  • 基于 Spring Boot 与 Vue 的在线考试系统设计与实现
  • 链表基础算法总结与经典题目解析
  • Kali Linux 系统安装与基础配置指南
  • 数据结构:堆、哈希表与字符串哈希详解
  • Java Web 开发环境搭建:IDEA 与 Tomcat 安装部署指南
  • Flutter 三方库 discord_interactions 在 OpenHarmony 上的适配指南
  • C++ 核心基础概念梳理:命名空间、引用与重载
  • Python 鼻炎在线预约挂号评价系统微信小程序设计与开发
  • 无人机经典教材 MAVSim 仿真资源与代码实践
  • Java WebFlux 集成百度地图深度检索实践
  • C++ 笔试刷题 Day 17 算法题解析
  • POJ 3984 迷宫问题最短路径求解
  • ARP 与 NAT 协议深度解析:原理与区别

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online