本文是对 Virtual Scrolling for Billions of Rows — Techniques from HighTable 的精华提炼。原作者 Severo Ochoa,项目地址 hyparam/hightable。强烈建议阅读原文,包含交互式 demo 和可视化图解。


问题

在浏览器里显示一个两行两列的表格是 HTML 101。但当数据量到十亿行时,一切都会崩:内存撑不住、DOM 节点爆炸、滚动条精度不够。

HighTable 是一个 React 组件,用五种递进式技术解决了这个问题 — 而且全程使用原生 HTML 元素,没有 fake scrollbar,没有 canvas 渲染。

五个技巧

1. 懒加载 (Lazy Loading)

问题: 10B 行 × 100 bytes/行 = 1TB,不可能全部加载。

方案: 只加载当前可见的 ~30 行。通过 DataFrame 抽象层按需 fetch + 缓存,新数据就绪时触发 resolve 事件重新渲染。

const rowStart = Math.floor(firstVisiblePixel / rowHeight)
const rowEnd = Math.ceil(lastVisiblePixel / rowHeight)

效果: 1TB 数据只需 ~3KB 内存。

2. 表格切片 (Table Slice)

问题: 100 万行 = 100 万个 <tr>,Chrome 建议不超过 300 个 DOM 节点。

方案: 在 viewport 和 table 之间插入一个 canvas div(不是 <canvas> 元素),设置为全表高度以撑出正确的滚动条。table 只渲染可见行,用 position: absolute + 动态 top 定位。

canvas.style.height = `${numRows * rowHeight}px`
table.style.top = `${scrollTop - (scrollTop % rowHeight)}px`

效果: 不管多少行,DOM 节点恒定 ~30 个。

3. 无限像素 (Infinite Pixels)

问题: 浏览器对元素高度有上限(Firefox ~17M px)。33px 行高 → 最多 50 万行。

方案: 设置 canvas 最大高度(8M px),超出后用缩放因子映射滚动位置到全表位置。

downscaleFactor = (fullTableHeight - clientHeight) / (maxCanvasHeight - clientHeight)
firstVisibleRow = Math.floor((scrollTop * downscaleFactor) / rowHeight)

代价: 滚动精度下降。10B 行时,滚动 1px ≈ 跳过 ~7300 万行。某些行通过滚动条不可达。

4. 像素级精确滚动 (Pixel-precise Scroll)

问题: 技巧 3 让每次滚动都是”全局跳转”,无法逐行浏览。

方案: 双模式滚动 — 维护 { globalAnchor, localOffset } 状态。小幅滚动(鼠标滚轮)= local,累加 offset;大幅滚动(拖动滚动条)= global,重置 anchor。

if (Math.abs(delta) > threshold) {
  state.localOffset = 0           // global jump
  state.globalAnchor = scrollTop
} else {
  state.localOffset += delta      // local fine scroll
}

效果: 行高 30px 时,2 万亿行以内保证 1px 精度。64 万亿行以内每行可达。

5. 两步随机访问 (Two-step Random Access)

问题: 键盘导航(↓ / Ctrl+End)可能跳转到不在 DOM 中的行。

方案: 分离垂直和水平滚动。先更新状态 → 程序化垂直滚动 → 渲染目标行到 DOM → 水平滚动到目标列 → focus。用 flag 防止程序化滚动触发多余的水平滚动。

关键:程序化滚动必须用 behavior: 'instant',避免 smooth 产生的中间状态冲突。

效果: 键盘可达任意单元格,即使十亿行。

核心洞察

这五个技巧是递进式的,每一层解决上一层引入的新问题:

技巧解决引入
懒加载内存不够
表格切片DOM 节点太多
无限像素浏览器高度上限滚动精度丢失
双模式滚动精度丢失状态复杂度
两步随机访问键盘导航断裂

最值得注意的设计决策:全程使用原生 HTML 元素。没有 fake scrollbar,没有 <canvas> 渲染,依赖 Web 平台的 overflow-y: autoposition: stickyscrollIntoView。这让组件天然保留了浏览器的无障碍支持(WAI Grid Pattern + tabindex roving)。

链接

相关文章

加入讨论

分享您的想法,与其他读者交流。评论通过 GitHub 管理,确保安全可靠。

请遵守我们的 评论政策,共同维护良好的讨论环境