前端如何优雅展示 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

最终结论:

  • 小数据 → 下拉追加就够了;
  • 大数据(上万条) → 必须虚拟列表,否则用户体验完全不可接受。
相关推荐
用户69371750013843 小时前
Google 正在“收紧侧加载”:陌生 APK 安装或需等待 24 小时
android·前端
蓝帆傲亦3 小时前
Web 前端搜索文字高亮实现方法汇总
前端
用户69371750013843 小时前
Room 3.0:这次不是升级,是重来
android·前端·google
漫随流水4 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
踩着两条虫6 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
jzlhll1237 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
蓝冰凌7 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛8 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
柳杉8 小时前
从动漫水面到赛博飞船:这位开发者的Three.js作品太惊艳了
前端·javascript·数据可视化