虚拟列表:高频核心面试题 + 追问链

1. 虚拟列表和普通列表懒加载的核心区别是什么?

面试者真正应该说出口的答案

普通列表懒加载只是把 DOM 的创建过程分批进行,用户滚得越远,DOM 通常越积越多;虚拟列表只维护可视区域和附近的一小批 DOM,滚动时复用或更新这些节点,所以 DOM 数量基本不会随着数据总量增长。

这句话是这道题最核心的区分。

面试官继续追问后的展开

比如有 10 万条数据:

普通列表懒加载:

text 复制代码
滚动
 ↓
加载下一批数据
 ↓
数据越来越多
 ↓
DOM 也越来越多
 ↓
最终还是可能卡

它优化的是:

网络请求和数据加载成本。

而虚拟列表:

text 复制代码
10 万条数据
    ↓
数据可以全部存在内存
    ↓
根据滚动位置计算当前应该显示哪些数据
    ↓
只创建这一小段数据对应的 DOM
    ↓
滚动时不断复用/更新这部分 DOM

它优化的是:

DOM 数量和渲染、布局、绘制相关成本。

所以二者甚至可以同时使用:

text 复制代码
分页 / 懒加载
    ↓
控制数据加载量

虚拟列表
    ↓
控制 DOM 数量

2. 为什么普通列表数据一多就会卡?

真正的问题不是"数据多",而是大量数据最终变成了大量 DOM,而 DOM 增多会带来渲染、布局、绘制以及更新成本。

面试官继续追问后的展开

例如:

html 复制代码
<ul>
  <li>第 1 条</li>
  <li>第 2 条</li>
  ...
  <li>第 100000 条</li>
</ul>

100000 条数据如果全部渲染,就意味着浏览器需要维护大量节点。

问题会逐层出现:

text 复制代码
数据量大
 ↓
DOM 节点多
 ↓
样式计算 / Layout / Paint 等成本增加
 ↓
首次渲染慢
 ↓
更新成本增加
 ↓
滚动、交互容易出现卡顿

所以虚拟列表真正解决的是:

让"数据量"和"DOM 数量"解耦。

这是比"减少 DOM"更重要的一层理解。


3. 虚拟列表到底是怎么实现的?

这是第一道真正的技术分水岭。

核心就是根据现在滚到的位置,算出这一屏要显示哪些数据,只渲染这一小部分 DOM,剩下的用空白区域把滚动条撑起来。

面试官继续追问后的展开

假设:

text 复制代码
总数据:10000 条
每一项高度:50px

那么整个列表理论高度:

text 复制代码
10000 × 50 = 500000px

但实际上屏幕可能只能显示:

text 复制代码
20 条左右

所以页面不需要真的创建 10000 个 DOM。

可以变成:

text 复制代码
┌─────────────────────┐
│      空白占位       │ ← 上方 spacer
├─────────────────────┤
│ item 101            │
│ item 102            │
│ item 103            │
│ ...                 │ ← 真正渲染
│ item 120            │
├─────────────────────┤
│      空白占位       │ ← 下方 spacer
└─────────────────────┘

核心就是三个东西:

text 复制代码
总高度
+
当前可视范围
+
可视范围对应的数据

4. 固定高度虚拟列表怎么计算?

这是非常高频的追问。假设:

text 复制代码
itemHeight = 50px
scrollTop = 5000px

那么当前滚动到了:

text 复制代码
5000 / 50 = 100

也就是说:

text 复制代码
startIndex ≈ floor(scrollTop / itemHeight)

假设 viewport 高度是:

text 复制代码
500px

那么可视数量大约:

text 复制代码
500 / 50 = 10

实际工程一般不会只渲染这 10 个,而是增加一个 buffer:

text 复制代码
startIndex
 ↓
前面多渲染几个

[buffer]
[可视区域]
[buffer]

例如:

text 复制代码
        scrollTop
            ↓
────────────┬────────────
            │
        item 100
        item 101
        item 102
        ...
        item 120
            │
────────────┴────────────

最终得到:

javascript 复制代码
const startIndex = Math.floor(scrollTop / itemHeight);

const visibleCount = Math.ceil(viewportHeight / itemHeight);

const endIndex = startIndex + visibleCount + buffer;

然后:

javascript 复制代码
const visibleItems = list.slice(startIndex, endIndex);

这就是最基础的固定高度虚拟列表。


5. 为什么还需要一个"占位容器"?

因为实际只渲染了一小部分 DOM,需要用占位空间撑起完整列表高度,保证滚动条正常。

比如:

text 复制代码
10000 条 × 50px
= 500000px

我们实际只渲染:

text 复制代码
20 条

如果没有占位高度:

text 复制代码
浏览器看到的高度:

20 × 50 = 1000px

滚动条当然就只有这么长。所以虚拟列表通常会:

css 复制代码
.list {
  height: 500000px;
}

然后真正的内容通过:

css 复制代码
transform: translateY(...)

或者:

css 复制代码
top: ...

移动到正确的位置。所以你可以把它理解成:

text 复制代码
完整列表的"空间"存在
完整列表的"DOM"不存在

这句话非常适合面试。


6. 滚动的时候发生了什么?

滚动时根据新的 scrollTop 重新计算 startIndex 和 endIndex,然后更新这一小段 DOM 的内容和位置。

完整流程:

text 复制代码
用户滚动
   ↓
scrollTop 变化
   ↓
计算 startIndex
   ↓
计算 endIndex
   ↓
得到新的可视数据
   ↓
更新 DOM
   ↓
移动内容容器

例如:

text 复制代码
第一次:

startIndex = 100
endIndex   = 120

渲染:

100 ~ 120

继续往下滚:

text 复制代码
startIndex = 120
endIndex   = 140

于是:

text 复制代码
100 ~ 120
   ↓
120 ~ 140

这里还有一个非常重要的工程优化:

不一定真的销毁 100~120,再创建 120~140。

优秀的虚拟列表会尽可能:

text 复制代码
复用已有 DOM
+
更新数据
+
调整位置

所以它进一步减少了频繁创建和销毁 DOM 的成本。


7. 为什么虚拟列表滚动时不会出现明显跳动?

因为数据项的实际位置和滚动位置之间必须建立稳定的映射,固定高度可以直接通过索引计算,动态高度则需要额外维护每一项的真实高度。

固定高度非常简单:

text 复制代码
index × itemHeight

例如:

text 复制代码
第 100 项:

100 × 50 = 5000px

所以定位非常准确。


8. 如果每一项高度不固定怎么办?

这才是更高级的追问。

比如:

text 复制代码
item 1   50px
item 2   100px
item 3   80px
item 4   200px
...

这时候:

javascript 复制代码
index * itemHeight

已经不能用了。

因为:

text 复制代码
第 100 项的位置
≠
100 × 一个固定高度

通常需要:

text 复制代码
预估高度
   ↓
先计算一个大概位置
   ↓
真正渲染
   ↓
测量真实高度
   ↓
更新高度缓存
   ↓
修正后续位置

高度缓存可能类似:

javascript 复制代码
heightMap = {
  0: 50,
  1: 100,
  2: 80,
  3: 200
};

然后通过累计高度计算某个 index 的实际位置。数据量非常大时,还会进一步使用:

text 复制代码
前缀和
二分查找
Fenwick Tree / Segment Tree

等数据结构优化"根据滚动位置反查 index"的成本。

这就是动态高度虚拟列表比固定高度复杂很多的原因。


9. 虚拟列表真正优化的是什么?

text 复制代码
数据量 N
   ↓
普通列表
N 条数据 → N 个 DOM
   ↓
渲染 / Layout / Paint / 更新成本随 N 增长

虚拟列表

N 条数据
   ↓
只保留 O(k) 个 DOM
k ≈ 可视区域 + buffer
   ↓
DOM 数量与总数据量解耦

所以它最核心的价值是:

把 DOM 的规模从跟数据总量相关,变成跟可视区域相关。

这比"减少 DOM"更接近底层。


10. 虚拟列表是不是一定比普通列表快?

不是。 这是非常容易被追问的边界问题。

如果只有:

text 复制代码
20 条数据

你上虚拟列表反而可能增加复杂度:

text 复制代码
scroll 监听
+
索引计算
+
DOM 复用
+
位置计算
+
边界处理

所以虚拟列表不是:

"任何列表都应该使用。"

而是:

当列表数据量足够大,普通渲染已经成为瓶颈时,虚拟列表才有明显收益。


11. 虚拟列表的主要矛盾是什么?

主要矛盾

数据量很大,但用户同一时刻真正能看到的数据很少。

普通列表:

text 复制代码
100000 条数据
        ↓
100000 个 DOM

虚拟列表:

text 复制代码
100000 条数据
        ↓
用户只看到几十条
        ↓
只维护几十到几百个 DOM

所以它实际上是在解决:

数据规模和可视 DOM 规模不匹配的问题。


12. 虚拟列表有哪些工程边界?

高级面试可以继续追问:

① 动态高度

最麻烦的问题之一:

text 复制代码
真实高度未知
→ 位置不好计算
→ 高度变化可能导致滚动位置修正

② 快速滚动

用户快速拖动滚动条:

text 复制代码
scrollTop
100
500
2000
10000
50000

如果每次 scroll 都同步计算和更新 DOM,可能造成新的性能问题。

通常需要:

text 复制代码
节流
+
requestAnimationFrame
+
buffer
+
DOM 复用

③ 大量复杂子组件

虚拟列表只能减少:

text 复制代码
DOM 数量

如果每个 item 本身就非常复杂:

text 复制代码
Item
 ├── React/Vue 组件
 ├── 图片
 ├── 计算
 ├── 状态
 └── 事件

仍然可能卡。

④ 图片加载

虚拟列表不能自动解决:

text 复制代码
图片请求
图片解码
图片绘制

所以通常还需要:

text 复制代码
图片懒加载
+
尺寸预留
+
缓存

⑤ 无障碍和搜索

虚拟列表只存在部分 DOM,因此:

text 复制代码
键盘导航
焦点管理
浏览器查找
无障碍阅读器

都需要额外处理。


13. 最后一道:如果让你自己实现一个虚拟列表,你怎么设计?

这道题非常适合拉开 3 年和 5 年+ 前端的差距。

我会先确定列表是固定高度还是动态高度;固定高度直接通过 scrollTop 和 itemHeight 算可视索引,动态高度则需要做高度缓存和位置修正,然后通过上下占位空间保持完整滚动高度,实际只维护可视区域加 buffer 的 DOM。

然后展开:

text 复制代码
                Virtual List
                     │
        ┌────────────┴────────────┐
        ↓                         ↓
   固定高度                    动态高度
        │                         │
scrollTop / itemHeight       height cache
        │                         │
直接计算 index              计算累计高度
        │                         │
        └────────────┬────────────┘
                     ↓
              startIndex
                     ↓
               endIndex
                     ↓
          visibleItems + buffer
                     ↓
                DOM 复用
                     ↓
             translateY 定位
                     ↓
             spacer 撑起总高度

14. 最终满分答案

面试官问:虚拟列表是什么?为什么需要它?

虚拟列表就是:数据很多,但 DOM 不需要全部创建,只根据当前滚动位置渲染可视区域附近的一小部分数据。

普通列表的问题是,数据量一大,最终会变成大量 DOM,进而增加渲染、布局、绘制和更新成本。虚拟列表把这两个东西解耦:

text 复制代码
数据可以有 10 万条
        ↓
实际 DOM 只保留可视区域 + buffer
        ↓
滚动时根据 scrollTop 重新计算数据范围
        ↓
更新并复用这部分 DOM

同时用占位空间撑起完整列表高度,让浏览器的滚动条仍然对应 10 万条数据。

固定高度的情况下,可以直接通过:

text 复制代码
startIndex ≈ scrollTop / itemHeight

计算当前应该渲染哪些数据;如果是动态高度,就需要额外维护每一项的真实高度和位置。

所以虚拟列表最核心的价值不是简单的"优化长列表",而是:

让 DOM 数量从跟总数据量相关,变成跟可视区域相关。

而普通列表懒加载解决的是"什么时候加载数据",虚拟列表解决的是"什么时候创建和保留 DOM",两者可以一起使用。

这套答案已经能从"会背概念"进入到"理解底层实现"的层次。真正的大厂追问,重点就落在:scrollTop → index → 可视数据 → DOM 复用 → 定位 → 动态高度 这条链上。

相关推荐
❆VE❆6 个月前
虚拟列表原理与实战运用场景详解
前端·javascript·css·vue.js·html·虚拟列表
木斯佳6 个月前
前端八股文面经大全:字节广告交易前端一面(2026-03-31)·面经深度解析
前端·markdown·虚拟列表·流式数据
被考核重击6 个月前
虚拟列表(动态高度,性能优化,骨架屏)
javascript·vue.js·性能优化·虚拟列表
午安~婉8 个月前
构图跟拍相关
前端·javascript·拍照·虚拟列表
Mike丶1 年前
【渲染优化】动态调整虚拟列表刷新率:让代码学会"偷懒"
性能优化·虚拟列表·动态调整·渲染优化
一只小白菜~2 年前
记录一下vue2项目优化,虚拟列表vue-virtual-scroll-list处理10万条数据
javascript·vue.js·虚拟列表
南城巷陌3 年前
react-virtualized实现行元素不等高的虚拟列表滚动
react.js·虚拟列表·virtualizedlist