R52第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

大列表与虚拟化

大列表与虚拟化 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能解释列表卡顿卡在 DOM 数量而非数据数量:实现一个最小虚拟列表(可视窗口 + 撑高容器 + 偏移计算),对比分页与虚拟化的适用边界,并验证滚动流畅与键盘定位正确。

问题场景

审计日志页要一次展示 2 万条记录。整表渲染:首屏数秒,滚动掉帧。第一反应是分页——但用户要 Ctrl+F 全文搜索和连续滚动浏览,分页把体验切断了。问题的本质:数据 2 万条不贵,贵的是 2 万个 DOM 节点。浏览器一次只需要看得见的几十条,其余都是白渲染。

核心解释

虚拟化的三个零件:

function VirtualList({ items, rowHeight, height }) {
  const [scrollTop, setScrollTop] = useState(0);
  const total = items.length * rowHeight;                           // 1. 撑高:真实总高度
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - 3); // 2. 可视窗口起点(含缓冲 3 行)
  const count = Math.ceil(height / rowHeight) + 6;
  const slice = items.slice(start, start + count);

  return (
    <div style={{ height, overflow: "auto" }} onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}>
      <div style={{ height: total, position: "relative" }}>
        <div style={{ position: "absolute", top: start * rowHeight, left: 0, right: 0 }}>
          {slice.map((it) => (
            <div key={it.id} style={{ height: rowHeight }}>{it.text}</div>
          ))}
        </div>
      </div>
    </div>
  );
}

无论数据多少,DOM 里永远只有「可视区 + 缓冲」几十行;滚动只是窗口起点在变。代价有两条:随机访问(跳到第 15000 条要先算偏移再滚过去)和变高行(等高实现最简单,变高行要维护位置索引,复杂度上一个台阶)。

分页与虚拟化的选择:用户要「连续浏览 + 全文检索 + 快速定位」,虚拟化;用户按「页」的心智消费数据、后端也按页出接口,分页;两者可叠加(虚拟化 + 增量加载)。键盘定位是虚拟化的经典坑:Ctrl+F、Tab、屏幕阅读器都「看不见」窗口外的行,选中项必须主动滚进窗口:

useEffect(() => {
  const el = document.getElementById("row-" + selectedId);
  if (el) el.scrollIntoView({ block: "nearest" });
}, [selectedId]);

示例拆解

实验区 2 万条日志两种渲染对照:

  • 「虚拟列表」开关:开启时页面显示「本次渲染行数」约 30 行;关闭则整表渲染 2 万行(第一次切换会明显卡几秒,这正是要演示的);
  • 「跳转到第 N 条」:虚拟模式下瞬间定位并把该行滚入视野中央;整表模式跳转代价极高(输入框被禁用,先体验再开虚拟);
  • 键盘:点击滚动容器获得焦点后,↑/↓ 移动选中行,选中接近边缘时窗口自动跟随;
  • 验证:对比两种渲染的行数与滚动手感;键盘把选中走到第 19000 条,确认它滚进视野。

动手任务

打开本课实验区:

  1. 把缓冲从 3 行改成 0,快速滚动观察边缘白条闪现,再改回——理解缓冲区的作用。
  2. 把 rowHeight 改成 48,确认偏移计算仍然正确(体会公式里 rowHeight 是唯一耦合点)。
  3. 给「跳转」加键盘支持:输入条数后按 Enter 跳转并把该行选中。

常见错误

  • 忘了撑高容器:没有 total 高度占位,滚动条长度错误,滚到一半就「到底」。
  • 窗口计算漏了缓冲:快速滚动时边缘频繁出现尚未渲染的白条;缓冲 3-5 行兜底。
  • 键盘或查找功能没做滚动跟随:选中了窗口外的行,用户看着「没反应」。
  • 行组件内部又渲染大子树:窗口里只有几十行,但单行成本过高,窗口行数 × 单行成本重新变大;行本身也要轻。

检查与复盘

自查清单:

  • 能说清撑高、窗口、缓冲三个零件各自的作用
  • 2 万条数据下,渲染行数与滚动流畅度验证过
  • 键盘选中窗口外的行能正确滚入视野

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

一个 10000 行的列表滚动卡顿,改用虚拟化(react-window 之类)后流畅了。虚拟化的基本原理是?

Q2读代码较难

阅读虚拟化列表的核心计算(React 18+)。容器可视高度 200px,每行高 50px,overscan=2,总行数 1000,当前 scrollTop=1200。startIndex 和 endIndex(含)分别是多少?实际渲染几行?

const ROW = 50;
const overscan = 2;

function getRange(scrollTop, viewportH, total) {
  const first = Math.floor(scrollTop / ROW);
  const visibleCount = Math.ceil(viewportH / ROW);
  const startIndex = Math.max(0, first - overscan);
  const endIndex = Math.min(total - 1, first + visibleCount + overscan - 1);
  return { startIndex, endIndex };
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...