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

前端虚拟列表核心原理与 React 实战实现

面对万级数据渲染导致的页面卡顿问题,虚拟列表通过仅渲染可视区域节点配合占位高度解决。解析其计算起始索引、偏移量及总高度的核心逻辑,提供基于 React Hooks 的完整 TypeScript 实现方案,并探讨使用 requestAnimationFrame 优化滚动性能的关键细节。

不知所云发布于 2026/4/10更新于 2026/7/2445 浏览

虚拟列表解决的问题

在实际后台系统中,我们经常遇到这样的场景:用户列表有 10 万条数据,订单列表 20 万条,日志甚至达到百万级。表格里往往还包含多列、复杂 DOM 结构、hover 交互、操作按钮和状态标签。

如果直接通过 map 渲染所有数据:

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

你会面临首次渲染卡死、滚动严重掉帧、内存暴涨,甚至浏览器直接崩溃的风险。

核心瓶颈其实很简单:DOM 节点太多。浏览器并不怕 JavaScript 的计算量,最怕的是成千上万个 DOM 节点同时存在。

虚拟列表的本质就是只渲染可视区域内的列表项,其余部分用占位高度'假装存在',从而保持 DOM 数量恒定。

核心设计思路

理解虚拟列表,关键在于把握以下四点:

  1. 可视区域(viewport):屏幕当前能看到的实际高度。
  2. 列表总高度:假设所有 item 都渲染后的总高度(虽然不真实渲染,但必须计算出来以撑开滚动条)。
  3. 起始索引和结束索引:根据当前的滚动距离,计算出应该显示哪几条数据。
  4. 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置。

实现原理详解

假设每一项高度固定,这是最简单的情况,实际项目中大量列表数据通常也是高度固定的。

例如:itemHeight = 50px,容器高度 500px。

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

通常会多渲染几条作为缓冲区(buffer),防止滚动过快出现白屏:实际渲染 = 10 + 4 = 14 条。

计算逻辑

根据滚动距离算索引:

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

不渲染所有 DOM,但要让滚动条是对的。我们需要一个占位元素撑开高度:

<div>
  <div></div> <!-- 撑开高度 -->
  <div></div> <!-- 只放可见项 -->
</div>

CSS 样式上:

 { : totalCount * itemHeight; }

目录

  1. 虚拟列表解决的问题
  2. 核心设计思路
  3. 实现原理详解
  4. 计算逻辑
  5. 代码实战:React 组件实现
  6. 如何使用
  7. 细节优化与注意事项
  8. 性能进阶:节流处理
  • 免费图片AI生成工具免费生成了解详情
.phantom
height

然后偏移当前渲染区域:

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

最终效果是:DOM 只有十几条,但滚动条像真的有十万条一样流畅。

代码实战:React 组件实现

下面是一个基于 React Hooks 的完整 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)}
          
        ))}
      
    
  )
}

如何使用

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. 为什么使用 transform 而不是 top? 因为 transform 不会触发布局重排(Reflow),性能比设置 top 属性好得多。

    transform: translateY(offsetY)
    
  3. Buffer 的意义 是为了防止滚动过快出现白屏,提前渲染上下几条数据。

    buffer = 5
    
  4. Key 的唯一性 为什么 key 用 startIndex + index?因为同一条数据在不同 scrollTop 下会复用 DOM,key 必须全局唯一,避免 React 误判导致状态丢失。

性能进阶:节流处理

在高频滚动场景下,建议对 scroll 事件进行节流处理。利用 requestAnimationFrame 能更平滑地同步状态,避免频繁触发渲染。

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

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

这样既能保证滚动流畅,又能有效控制渲染频率。

  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Java的数据类型与运算符详解
  • 安卓 Termux 部署 AstrBot 与 NapCat 搭建 QQ 机器人
  • Windows 系统 WSL2 Ubuntu 部署 OpenClaw
  • Claude 接入 MiniMax AI 大模型配置指南
  • AirSim 无人机物理引擎与动力学模拟:碰撞、风场、噪声及校准
</div>
</div>
</div>
  • Phi-3-vision-128k-instruct 开源镜像:支持国产昇腾/寒武纪平台适配指南
  • Llama-Factory 自定义损失函数实现指南
  • ArcGIS SDE 数据库锁表解锁实战指南
  • IntelliJ IDEA 实用插件:GitToolBox 使用指南
  • Nginx-1.28.0 Windows 版安装包及核心功能说明
  • AI前端之路:差异、技能栈与从零进阶
  • Java 线程与锁:JLS 第 17 章核心机制解析
  • 基于腾讯云部署 OpenClaw 并接入飞书指南
  • Vitis AI 模型 FPGA 部署实战:从训练到板端推理
  • 使用 Coze 低代码搭建 AI 小程序实现零编程变现
  • Swin Transformer 架构解析及在 UCI-HAR 行为识别中的应用
  • Whisper-large-v3 常见问题解析与语音识别避坑指南
  • 国内大模型公司面试经验与考点总结
  • 后仿真 SDF 反标常见 Warning 分析与处理指南
  • AI 四大核心技术详解:LLM、Agent、RAG 与 Skill
  • 相关免费在线工具

    • 加密/解密文本

      使用加密算法(如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