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

最终结论:

  • 小数据 → 下拉追加就够了;
  • 大数据(上万条) → 必须虚拟列表,否则用户体验完全不可接受。
相关推荐
这是个栗子3 分钟前
AI辅助编程(一) - ChatGPT
前端·vue.js·人工智能·chatgpt
2501_944448003 分钟前
Flutter for OpenHarmony衣橱管家App实战:预算管理实现
前端·javascript·flutter
Remember_9936 分钟前
Spring 核心原理深度解析:Bean 作用域、生命周期与 Spring Boot 自动配置
java·前端·spring boot·后端·spring·面试
笨蛋不要掉眼泪9 分钟前
Redis持久化解析:RDB和AOF的对比
前端·javascript·redis
心.c12 分钟前
Vue3+Node.js实现文件上传分片上传和断点续传【详细教程】
前端·javascript·vue.js·算法·node.js·哈希算法
We་ct13 分钟前
LeetCode 48. 旋转图像:原地旋转最优解法
前端·算法·leetcode·typescript
黄筱筱筱筱筱筱筱23 分钟前
7.适合新手小白学习Python的异常处理(Exception)
java·前端·数据库·python
Yeats_Liao29 分钟前
微调决策树:何时使用Prompt Engineering,何时选择Fine-tuning?
前端·人工智能·深度学习·算法·决策树·机器学习·prompt
晚霞的不甘30 分钟前
Flutter for OpenHarmony 实现 iOS 风格科学计算器:从 UI 到表达式求值的完整解析
前端·flutter·ui·ios·前端框架·交互
陈希瑞33 分钟前
OpenClaw Chrome扩展使用教程 - 浏览器中继控制
前端·chrome