🎬 面试开场怎么引出这个话题
「在密评系统里有一个测评数据表,经常要展示 3 万多行的指标数据,每行还有编辑、校验、下拉选择。一开始用普通表格,直接白屏 5 秒,Chrome 还提示"页面无响应"。后来换成 vxe-table 的虚拟滚动,只渲染可视区的 20 多行,首屏从白屏到可交互只要 200ms。中间踩了不少坑 — 比如固定列和虚拟滚动一起用会错位行高不一致时索引计算错位。」
✅ 抛出 4 个可深挖的点:① 数据规模(3 万行)② 白屏根因 ③ 虚拟滚动原理 ④ 固定列错位/行高坑。
表格是后台系统最高频的组件,虚拟滚动原理是中高级前端必问。
1

为什么 3 万行会白屏?虚拟滚动原理

⚠️ 白屏根因

现象: 3 万行数据塞进 DOM,页面卡死 5 秒,Chrome 提示"页面无响应"。

根因(不是网络、不是 JS 慢): DOM 节点太多。3 万行 × 每行 8 列 = 24 万个 td,加上文本节点、wrapper,DOM 树有上百万节点。浏览器布局(Layout)和绘制(Paint)阶段直接爆炸 — 这两个阶段无法异步,主线程被锁死。

关键认知: 性能瓶颈不在"数据多",而在"渲染的 DOM 多"。哪怕数据加载瞬间完成,只要把它全渲染成 DOM,浏览器就扛不住。虚拟滚动的核心 = 只渲染可视区的 DOM
🧠 类比理解
想象一个只有 20 个座位的电影院,要放 3 万人看的电影
普通表格 = 把 3 万个椅子全摆进去 → 电影院挤爆,谁都看不了。
虚拟滚动 = 只摆 20 个椅子,谁滚到屏幕前(可视区)就给谁座位,后面的人在外面排队(数据在内存里),离开的人把座位让出来。
DOM 椅子永远只有 20 个,但用户感觉有 3 万个 — 因为滚动时实时换人。
🔑 虚拟滚动核心实现
// 虚拟滚动的三个核心计算
const visibleCount = 20           // 可视区能显示几行(容器高 / 行高)
const bufferCount = 5             // 上下缓冲行数,防止滚动时白屏
const rowHeight = 40              // 每行固定高度(px)
const total = allData.length         // 总行数 30000

// 滚动时根据 scrollTop 算出该渲染哪几行
function onScroll(scrollTop) {
  // 1. 当前可视区起始索引
  const startIndex = Math.floor(scrollTop / rowHeight) - bufferCount
  const safeStart = Math.max(0, startIndex)

  // 2. 结束索引
  const endIndex = safeStart + visibleCount + bufferCount * 2

  // 3. 只 slice 这一段数据渲染
  const visibleData = allData.slice(safeStart, endIndex)

  // 4. 用 translateY 把这几行推到正确位置
  const offsetY = safeStart * rowHeight
  content.style.transform = `translateY(${offsetY}px)`
}

// 5. 容器的总高度要撑开,让滚动条长度正确
// 用一个占位 div:height = total * rowHeight
spacer.style.height = (total * rowHeight) + 'px'
三个关键点:占位 div 撑高:让滚动条长度像有 3 万行一样;
translateY 定位:实际渲染的 20 行通过 transform 推到可视位置;
缓冲行:上下各多渲染几行,快速滚动时不至于看到空白。
最终 DOM 永远只有 ~30 个 td,无论数据多少。
2

选型:ag-grid vs vxe-table

维度ag-gridvxe-table
背景国外,企业级国产,开源
虚拟滚动✅ 企业版才完整支持✅ 免费版就有
固定列/表头✅ 强✅ 强
单元格编辑✅ 功能全✅ 功能全
中文文档❌ 一般✅ 详细
体积大(企业版更大)
授权企业版收费MIT 免费
我们项目合规平台用(需要企业级)密评系统用(免费够用)
选型结论: 如果是商业项目且预算允许,ag-grid 企业版功能最全(行分组、主从表、范围选择都强);如果是内部系统、对成本敏感,vxe-table 免费版完全够,而且中文文档好、社区活跃。我们两个项目分别用了两个,都有实战。
3

固定列 + 虚拟滚动 = 错位地狱

⚠️ 真实问题

现象: 表格有左侧固定列(比如"指标名称"列固定),开了虚拟滚动后,横向滚动时固定列和主表格的行错位 — 固定列显示的是第 5 行数据,主表格显示的是第 8 行。

根因: 固定列和主表格是两个独立的虚拟滚动容器,各自算 startIndex。如果两个容器的 scrollTop 同步有延迟(或缓冲行数不一样),渲染的行就不一致。

解法: ① 两个容器共用同一个 scrollTop 监听,同步 startIndex;② 缓冲行数必须一致;③ 滚动事件用 throttle(16ms / 一帧),避免高频计算不同步。
经验: 固定列 + 虚拟滚动 + 行高不固定 是表格三大地狱组合。能避免就避免 — 比如行高统一固定 40px,能用固定行高就别用动态行高。实在要动态行高,见下一节。
4

行高不一致:索引计算的灾难

⚠️ 问题

有些测评指标的描述很长,一行显示不下,行高会自适应变高(比如有的 40px,有的 120px)。这时 startIndex = scrollTop / rowHeight 这个公式失效了 — 因为每行高度不同,不能简单除法。

本质: 定高虚拟滚动是 O(1) 算索引;变高虚拟滚动要 O(n) 累加每行高度,或维护一个高度前缀和数组(二分查找)。复杂度上一个台阶。
📋 变高虚拟滚动的两种方案
方案原理优缺点
预估高度 + 实测修正先用预估高度算位置,渲染后测量真实高度,更新位置缓存✅ 通用 ✅ 渐进准确 / ⚠️ 滚动时会有轻微跳动
高度前缀和 + 二分维护每行高度的前缀和数组,二分找 startIndex✅ 精确 / ⚠️ 需要预先知道所有行高(不现实)
实战建议:能用固定行高就用固定行高 — 内容超长用省略号 + tooltip 展示完整;
② 实在要变高,vxe-table 和 ag-grid 都内置了"预估高度 + 修正"机制,直接用它们的,别自己造轮子;
③ 给一个合理的预估高度(接近平均),减少跳动幅度。
5

单元格编辑、校验、性能

📋 万行表格的编辑场景

测评数据需要逐行编辑、下拉选择、校验。这里的坑:

表现解法
响应式劫持3 万行数据全 reactive,编辑卡用 shallowRef 存表格数据,只让编辑的单元格响应式
校验全量跑编辑一行触发全表校验,卡只校验编辑的行 + 保存时再全量校验
编辑状态丢失滚出可视区再回来,编辑中的值没了编辑值实时同步到数据源(不在 DOM 里存)
批量保存3 万行一次性 PUT,超时分片保存 / 只提交 dirty 行(改过的)
关键优化: 只提交改过的行(dirty rows)。维护一个 changedRowIds 的 Set,用户编辑时记录 id,保存时只发这些行。3 万行可能只改了 50 行,提交快 100 倍。
6

虚拟滚动之外:海量数据的其他手段

手段场景效果
分页(后端)常规列表根本解法 — 别把 3 万行拉前端
虚拟滚动必须看全量(如测评表)前端渲染减压
分片加载滚动到底加载下一批减少首屏数据量
列懒渲染列也很多(几十列)横向也虚拟化
Web Worker 处理前端要排序/聚合大数据不阻塞主线程
Time Slicing无法虚拟化的场景requestIdleCallback 分片渲染
第一原则: 能分页就别虚拟滚动。虚拟滚动是"用户必须看全量数据"时的妥协(比如测评要整体把握)。90% 的场景,后端分页 + 前端只显示一页 20 条,是最优解。
7

高频追问 & 容易翻车点

1虚拟滚动的原理是什么? 必问
核心: 只渲染可视区的 DOM。用一个占位 div 撑起总高度(让滚动条正确),实际只渲染可视区的 N 行(加缓冲行),通过 translateY 定位。
计算: startIndex = scrollTop / rowHeight;visibleData = data.slice(start, end)。
为什么快: DOM 节点从 3 万降到 ~30,Layout 和 Paint 不再是瓶颈。
💬 追问「为什么 DOM 多会卡?」→ 浏览器的 Layout(布局)和 Paint(绘制)是同步的,DOM 越多计算越久,主线程被锁死,这就是"页面无响应"。
2为什么不用分页? 中等
能分页就分页 — 后端分页是根本解法。但我们测评场景必须看全量:测评员要整体浏览所有指标、跨行对比、批量编辑。分页切来切去反而影响效率。所以这个场景用虚拟滚动更合适。
原则: 列表展示类 → 分页;整体操作类(表格编辑、对比) → 虚拟滚动。
3行高不固定怎么办? 中等
定高: startIndex = scrollTop / rowHeight,O(1)。
变高: 不能简单除法。两种方案:① 预估高度 + 渲染后实测修正(滚动时轻微跳动);② 维护高度前缀和数组,二分查找索引。
实践: 优先固定行高(内容超长用省略号 + tooltip);实在变高,用 vxe-table/ag-grid 内置的动态行高,别自己写。
4固定列 + 虚拟滚动为什么错位? 深问
固定列和主表格是两个独立虚拟容器,各自算 startIndex。如果 scrollTop 同步有延迟或缓冲行数不同,渲染的行不一致 → 错位。
解法: 共用同一个 scroll 监听同步 startIndex,缓冲行一致,scroll 事件 throttle 到一帧(16ms)。
53 万行数据怎么存?用 reactive 会卡吗? 中等
会卡。 3 万行全 reactive,Vue3 要给每个对象的每个属性装 Proxy,初始化慢,编辑时 diff 也慢。
解法: 用 shallowRef 存整体数据(和 ECharts 那个坑同理)。编辑单元格时,只让那一行的那个字段响应式,或用 triggerRef 手动触发。
💬 这题和大屏的 shallowRef 是同一原理,可以串联讲,体现一致性。
6ag-grid 和 vxe-table 怎么选? 基础
ag-grid: 企业级,功能最全(主从表、行分组、范围选择),但企业版收费,体积大。
vxe-table: 国产开源,免费版就有虚拟滚动和固定列,中文文档好。
原则: 商业项目预算够 → ag-grid 企业版;内部系统成本敏感 → vxe-table。我们两个项目分别用了两个。
8

30 秒电梯陈述(背熟)

🎤 30 秒版
「在密评系统有个测评数据表,3 万多行,每行还要编辑和校验。一开始用普通表格直接白屏 5 秒,Chrome 提示无响应。根因不是数据慢,是 DOM 太多 — 3 万行 × 8 列 = 24 万个 td,浏览器 Layout 阶段被锁死。换成 vxe-table 虚拟滚动后,只渲染可视区 20 多行,首屏 200ms 可交互。中间踩了固定列错位、行高不一致的坑 — 前者是两个虚拟容器 startIndex 不同步,后者是变高不能用简单除法算索引。」
这段话的优势:
① 有规模(3 万行)→ 真实
② 有根因(DOM 多导致 Layout 卡)→ 懂浏览器渲染
③ 有方案(虚拟滚动 + 具体坑)→ 有深度
④ 串联 shallowRef(和大屏话题呼应)→ 体系化
虚拟滚动是中高级前端分水岭,讲清楚原理 + 踩坑,基本稳了。

面试亮点专题 02 — 万行级表格虚拟滚动深度话题

虚拟滚动原理 · DOM 性能瓶颈 · ag-grid vs vxe-table · 固定列错位 · 变高行 · dirty 提交

← 返回专题首页