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

最终结论:

  • 小数据 → 下拉追加就够了;
  • 大数据(上万条) → 必须虚拟列表,否则用户体验完全不可接受。
相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60615 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角6 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!6 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi8 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马9 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端