前端如何优雅展示 1 万条数据?从下拉追加到虚拟列表的实战对比

1. 常见的做法:下拉追加数据

在实际开发中,我们常见的"无限加载"列表,基本逻辑是这样的:

xml 复制代码
<template>
  <div class="list">
    <div v-for="item in items" :key="item.id" class="list-item">
      {{ item.text }}
    </div>
    <button @click="loadMore">加载更多</button>
  </div>
</template>

<script setup>
import { ref } from "vue"

const items = ref([])

function generate(start, count) {
  return Array.from({ length: count }, (_, i) => ({
    id: start + i,
    text: `用户 ${start + i}`
  }))
}

function loadMore() {
  const start = items.value.length
  items.value.push(...generate(start, 20))
}

// 首次加载
loadMore()
</script>

问题在哪里?

  • 数据量小(几百条)时不卡;
  • 但当数据量达到 1 万条 时,DOM 元素 = 1 万个,渲染、回流、Diff 全部爆炸;
  • 滚动会出现 明显卡顿,甚至浏览器崩溃。

2. 真正的挑战:大数据量下的流畅滚动

如果需求是:

  • 一次展示上万条数据(例如聊天记录、订单列表);
  • 需要支持任意跳转(直接滚到第 5000 条);

👉 下拉追加方式完全不够用,因为浏览器要一直背着所有 DOM 节点。


3. 高性能虚拟列表:核心思想

虚拟列表的思路非常简单:

不管有多少条数据,始终只渲染视口中可见的部分 + 少量缓冲区。

假设视口高度 = 500px,单个 item = 50px → 最多可见 10 条,再加上下缓冲,总共渲染 20~30 个元素即可。

无论总数据量是 1,000 还是 100,000,渲染成本几乎一样。


4. 固定高度虚拟列表的实现

最简单的场景:每个 item 高度一致。

xml 复制代码
<template>
  <div ref="container" class="list" @scroll="onScroll">
    <!-- 总高度占位 -->
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <!-- 实际渲染区域 -->
      <div :style="{ transform: `translateY(${topOffset}px)` }">
        <div v-for="item in visibleItems" :key="item.id" class="item">
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from "vue"

const props = defineProps({ items: Array, itemHeight: Number, containerHeight: Number })
const scrollTop = ref(0)

const visibleCount = computed(() => Math.ceil(props.containerHeight / props.itemHeight) + 2)
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight))
const endIndex = computed(() => startIndex.value + visibleCount.value)

const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value))
const topOffset = computed(() => startIndex.value * props.itemHeight)
const totalHeight = computed(() => props.items.length * props.itemHeight)

function onScroll(e) {
  scrollTop.value = e.target.scrollTop
}
</script>

效果验证

  • 即使是 1 万条数据,DOM 元素始终保持在 20~30 个;
  • 滚动非常流畅。

5. 动态高度的挑战与解决

现实中,列表项往往不是固定高度:

  • 有的用户名字很短 → 一行就够;
  • 有的备注很长 → 占 3 行。

难点

  1. 无法简单用 index * itemHeight 来计算偏移;
  2. 滚动时需要知道 第 N 个元素的累计高度。

解决思路

  1. 用 ResizeObserver 动态监听每个 item 的真实高度;
  2. 用 前缀和数组 (prefix sum) 记录累计高度;
  3. 用 二分查找 根据 scrollTop 找到第一个可见元素。

核心代码:

ini 复制代码
const heights = ref(new Map())       // 记录每个 item 的高度
const prefixHeights = ref([0])       // 前缀和数组

function updateHeight(id, el) {
  if (!el) return
  const h = el.getBoundingClientRect().height
  heights.value.set(id, h)

  // 重建前缀和
  const arr = [0]
  for (let i = 0; i < props.items.length; i++) {
    arr[i + 1] = arr[i] + (heights.value.get(props.items[i].id) || 50)
  }
  prefixHeights.value = arr
}

function findStartIndex(scrollTop) {
  let low = 0, high = prefixHeights.value.length - 1
  while (low < high) {
    const mid = Math.floor((low + high) / 2)
    if (prefixHeights.value[mid] <= scrollTop) low = mid + 1
    else high = mid
  }
  return low - 1
}

6. 对比总结

方法 优点 缺点 适用场景
下拉追加 实现简单,分页习惯 DOM 无限膨胀,滚动卡顿 数据量 ≤ 500
虚拟列表 高性能,支持任意跳转 实现复杂 数据量 ≥ 1000

最终结论:

  • 小数据 → 下拉追加就够了;
  • 大数据(上万条) → 必须虚拟列表,否则用户体验完全不可接受。
相关推荐
java1234_小锋2 小时前
【技术专题】HTML5 网页技术 - HTML5 Canvas 基础以及动画入门
前端·html·html5
知了学历孙老师5 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
ShineWinsu7 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说8 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
程序员老赵10 小时前
Docker 部署青龙面板:轻松搭建定时任务管理平台
前端·docker·开源
Swot10 小时前
Asciidoctor 公式数字下标错位问题:原因分析与解决方案
前端
Swot10 小时前
leo-editor 中将 Markdown 内容转成 asciidoc 格式
前端
狒狒AI10 小时前
Remotion / GSAP+Three / HyperFrames / Skia 四栈实测:代码渲染知识视频的选栈方法(附开源技能)
前端·人工智能·aigc
修炼的dance10 小时前
浏览器里导出H.265,8.5秒和55.5秒差在硬件
前端
补码缩写10 小时前
WebCodecs选编码器,探测之后还要验解码和播放
前端