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

最终结论:

  • 小数据 → 下拉追加就够了;
  • 大数据(上万条) → 必须虚拟列表,否则用户体验完全不可接受。
相关推荐
i小杨35 分钟前
Mac 开发环境与配置操作速查表
前端·chrome
陈随易1 小时前
改变世界的编程语言MoonBit:背景知识速览
前端·后端·程序员
狂奔solar1 小时前
使用Rag 命中用户feedback提升triage agent 准确率
java·前端·prompt
诗书画唱1 小时前
【前端教程】从零开始学JavaScript交互:7个经典事件处理案例解析
前端·javascript·交互
跟橙姐学代码1 小时前
写Python的人,都应该掌握的高效写法(用了真的爽!)
前端·python·ipython
摸鱼一级选手1 小时前
uni-app 常用钩子函数:从场景到实战,掌握开发核心
前端·vue.js·uni-app
LikM1 小时前
Reflect ES6 新增的内置对象
前端·javascript
wanxy4201 小时前
关于Vue2中使用Web Worker【热更新】
前端
艾小码1 小时前
还在被JavaScript数据类型搞糊涂?一篇文章帮你彻底搞懂!
前端·javascript
鹏程十八少1 小时前
5. Android <卡顿五>优化RecyclerView 卡顿:一套基于 Matrix 监控、Systrace/Perfetto 标准化排查流程(卡顿实战)
前端