← 回總覽

【第 3671 期】面向数十亿行数据的虚拟滚动 —— HighTable 的核心技术解析

📅 2026-03-17 09:02 前端早读课 软件编程 23 分鐘 27803 字 評分: 91
虚拟滚动 React 性能优化 大数据展示 浏览器底层限制
📌 一句话摘要 本文深入解析了 React 组件 HighTable 如何通过五项核心技术突破浏览器限制,实现在 Web 端流畅展示与操作数十亿行数据的虚拟滚动方案。 📝 详细摘要 文章详细介绍了 HighTable 组件在处理海量数据(数十亿行)时采用的纵向滚动技术栈。核心挑战在于浏览器内存限制、DOM 渲染压力以及原生 HTML 元素的最大高度限制(如 Firefox 的 1700 万像素)。作者提出了五项关键技术:1. 懒加载数据帧,按需获取可见行;2. 表格切片,仅渲染视口内的 DOM 节点;3. 无限像素降采样,通过数学映射突破浏览器高度限制;4. 像素级精准滚动,利用“局部+全
Skip to main content ![Image 1: LogoBestBlogs](https://www.bestblogs.dev/ "BestBlogs.dev")Toggle navigation menu Toggle navigation menuArticlesPodcastsVideosTweetsSourcesNewsletters

⌘K

Change language Switch ThemeSign In

Narrow Mode

【第 3671 期】面向数十亿行数据的虚拟滚动 —— HighTable 的核心技术解析 ============================================

!Image 2: 前端早读课 前端早读课 @前端早读课

One Sentence Summary

This article delves into how the React component HighTable overcomes browser limitations with five core technologies to achieve a virtual scrolling solution that smoothly displays and operates billions of rows of data on the web.

Summary

The article details the vertical scrolling technology stack employed by the HighTable component when handling massive datasets (billions of rows). The core challenges lie in browser memory limitations, DOM rendering pressure, and the maximum height limit of native HTML elements (e.g., Firefox's 17 million pixels). The author proposes five key technologies: 1. Lazy loading data frames, fetching visible rows on demand; 2. Table slicing, rendering only DOM nodes within the viewport; 3. Infinite pixel downsampling, breaking through browser height limits via mathematical mapping; 4. Pixel-level precise scrolling, compensating for precision loss due to downsampling using a "local + global" dual-mode logic; 5. Two-step random access, enabling keyboard navigation and programmatic jumps for extreme data volumes. This solution is entirely based on native HTML elements, requires no Canvas rendering, and balances performance with accessibility.

Main Points

* 1. Implement on-demand lazy loading and caching mechanisms through DataFrames.Design asynchronous fetch and synchronous getCell interfaces to load only the cell data required for the current viewport and cache it in memory, preventing massive data from overwhelming browser memory. * 2. Utilize table slicing technology to maintain a constant number of DOM nodes.Introduce a canvas layer between the viewport and the table, rendering only about 30 visible rows of data, ensuring rendering overhead remains low regardless of the total data volume. * 3. Introduce a downscaleFactor to break through browser height limitations.Addressing the browser's maximum height limit of approximately 17 million pixels, when the data volume is too large, the scrollbar displacement is proportionally mapped to a larger virtual space, enabling infinite row navigation. * 4. Design a "local + global" dual-mode scrolling logic to solve the precision loss problem.By judging the scroll increment size, it automatically switches between global jumps and local pixel offsets, ensuring users can both quickly navigate and perform fine-grained row-by-row scrolling. * 5. Support complex keyboard interactions through two-step random access and scroll decoupling.Separate vertical and horizontal scrolling logic, use flags to control the execution order of programmatic scrolling, ensuring pixel-level cell focusing can still be achieved across billions of rows of data.

Metadata

AI Score

91

Website mp.weixin.qq.com

Published At Today

Length 7884 words (about 32 min)

Sign in to use highlight and note-taking features for a better reading experience. Sign in now

!Image 3

前言

介绍了<HighTable>中与纵向滚动相关的五项核心技术。HighTable 是一个 React 组件,能够在保持良好性能和可访问性的前提下,在表格中展示数十亿行数据。今日前端早读课文章由 @Sylvain Lesage 分享,@飘飘编译。

译文从这开始~~

!Image 4一卷受吉亚斯丁・苏丹・穆罕默德・伊本・苏丹・埃雷特纳委托制作的古兰经卷轴(tumar),由穆巴拉克沙赫・伊本・阿卜杜拉署名,产自安纳托利亚东部,年代为 1353—1354 年)

这是一篇长文,篇幅之长恰恰映射出在表格中渲染数十亿行数据的复杂性,以及我们在构建这个 React 组件时所付出的大量心血。

#### 引言

在表格中展示数据,几乎是所有 HTML 入门课程中最早接触的练习之一。 <table>   <thead>     <tr><th>姓名</th><th>年龄</th></tr>   </thead>   <tbody>     <tr><td>Alice</td><td>64</td></tr>     <tr><td>Bob</td><td>37</td></tr>   </tbody> </table> 然而,数据科学中屡见不鲜的情况是 —— 小规模下运转良好的方案,一旦数据量攀升便会不堪重负。

本文将逐一展示我们在<HighTable>React 组件中用于应对纵向滚动挑战的五项技术,它们使组件能够驾驭数十亿行数据。

该组件还提供了丰富的列操作(排序、隐藏、调整宽度)、行操作(选中)以及单元格操作(键盘导航、指针交互、自定义渲染)功能。如果你想深入了解,欢迎提问或直接查阅源码。 <HighTable>组件托管在 hyparam/hightable,由 Kenny Daniel 为 Hyperparam 项目创建,而我有幸参与了近一年的开发工作。 【第2856期】腾讯文档智能表格渲染层 Feature 设计

#### 演示

欢迎试用 HighTable 演示:

!Image 5

HighTable 同样被应用于 Parquet 文件查看器、source.coop 以及 Hyperparam 中:

!Image 6HighTable 嵌入 hyperparam.app 的界面截图

#### 滚动基础

在深入各项技术之前,我们先来了解标准 HTML 表格中滚动的工作原理。

HTML 结构由一个可滚动的容器和其内部的表格元素组成,我们将这个容器称为视口(viewport): <div class="viewport" style="overflow-y: auto;">   <table class="table">     ...   </table> </div> 在这个结构中,视口是一个固定高度的div,CSS 属性overflow-y: auto使得当表格高度超过视口时,会自动出现纵向滚动条。

在下方的交互组件中,上下滚动左侧方框,即可观察右侧方框如何同步模拟滚动效果。

> 如果你使用键盘,可以按 Tab 键聚焦到左侧方框,再用方向键 ⏶ 和 ⏷ 进行滚动。当然,你也可以使用鼠标滚轮、拖动滚动条,或在触摸屏上滑动。

!Image 7

交互组件:左侧为视口,右侧为模拟滚动效果的示意图

该组件被其固定尺寸的视口(蓝色边框)所界定。表格(金色边框)渲染在容器内部。由于表格高度超过了视口高度,仅有部分内容可见,纵向滚动条允许用户切换可见区域。内部的表格元素在视口中上下移动,从而产生滚动效果。

在右侧,我们则模拟了滚动效果,展示表格相对于视口的位置关系。

接下来,我们明确一些后续会用到的定义和公式:

* 1、本文中,我们假设viewport.clientHeight(可见区域的高度)为常量。在 HighTable 中,我们会实际测量它并响应窗口大小的变化。

* 2、viewport.scrollHeight(可滚动内容的总高度)等于table.clientHeight。两者都等于表格行数乘以行高: const rowHeight = 33 // 单位:像素 const numRows = data.numRows // 表格总行数 const height = numRows * rowHeight 本文中,我们假设行高和行数均为常量。在 HighTable 的实际实现中,我们会响应data.numRows(数据帧中的行数,即承载表格数据的数据结构)的变化,例如筛选时行数会发生改变;但我们假定行高是固定的(关于支持可变行高的需求,请参见 issue#395)。

* 3、viewport.scrollTop是表格顶部与视口顶部之间的像素距离。最小值0px对应表格顶部可见,最大值viewport.scrollHeight - viewport.clientHeight则对应表格底部可见。

* 4、可见像素范围可由视口的滚动位置推算得出: const firstVisiblePixel = viewport.scrollTop const lastVisiblePixel = viewport.scrollTop + viewport.clientHeight // firstVisiblePixel 为闭区间,lastVisiblePixel 为开区间 了解了这些基础知识后,让我们来看看如何应对大规模数据集。

#### 技术一:懒加载

处理大规模数据集时,首先面临的挑战是 —— 数据根本无法全部塞进浏览器内存。好消息是:你也不会想同时查看所有行的数据。因此,与其在启动时加载整个数据文件,不如只加载当前可见的单元格。 【第1436期】利用交叉观察器解锁懒加载新姿势

需要注意的是,数据的懒加载并不会改变表格的 HTML 结构。

下方的交互组件展示了懒加载的工作原理。上下滚动左侧方框,观察右侧单元格是如何按需加载的:

!Image 8

交互组件:左侧为视口,右侧展示懒加载过程

在表格中,仅加载可见的单元格。滚动时,新进入可视区域的单元格会被请求并在后台加载,数据就绪后即刻渲染。

为此,我们先计算出可见行的范围,然后只加载这些行: const rowStart = Math.floor(firstVisiblePixel / rowHeight) const rowEnd = Math.ceil(lastVisiblePixel / rowHeight) // rowStart 为闭区间,rowEnd 为开区间 在 HighTable 中,数据加载逻辑由数据帧(data frame)处理,通过data属性传入 React 组件: <HighTable data={data} /> 数据帧是一个对象,定义了如何按需加载(即获取和缓存)数据,以及如何在渲染时读取已加载的数据。详见types.ts中的 DataFrame TypeScript 类型定义。

以下是一个简化的 DataFrame 实现,它为单列生成随机数据,并添加一定延迟以模拟网络请求,同时将数据缓存在内存中: const cache = new Map() const eventTarget = new EventTarget() const numRows = 1_000_000 const data = {   numRows,   eventTarget,   // 同步返回缓存值(如果有的话)   getCell({ row }) {     return cache.get(row);   },   // 加载指定行范围内缺失的数据,并缓存   async fetch({ rowStart, rowEnd }) {     // 模拟网络延迟     await new Promise((resolve) => setTimeout(resolve, 100));     for (let row = rowStart; row < rowEnd; row++) {       // 跳过已缓存的行       if (cache.has(row)) continue;       // 为单元格生成随机值并缓存       cache.set(row, {value: Math.random()});     }     // 触发事件,通知 <HighTable> 重新渲染可见单元格     eventTarget.dispatchEvent(new Event('resolve'));   }, } 数据帧通过异步的data.fetch()方法从数据源加载数据,必须缓存结果,并在新数据可用时触发resolve事件。数据源可以是任何东西 —— 在我们的示例中,数据是随机生成的;也可以来自本地文件、内存数组、远程文件(通过 HTTP 范围请求),或 REST API,等等。

数据帧还必须提供一个同步的data.getCell()方法,用于获取指定单元格的缓存数据;若数据尚未加载,则返回undefined

每次滚动时,表格都会重新渲染,对可见行调用data.getCell(),同时在必要时调用data.fetch()在后台加载数据(数据帧有责任在数据已缓存的情况下快速返回)。每当新数据被获取并上报(通过resolve事件),表格便会重新渲染。

你可以在 hyparquet 演示 中找到一个更完整的数据帧示例,展示如何通过 HTTP 范围请求加载远程 Parquet 文件。 【第2160期】钉钉表格,从零到一打造在线 Excel

数据帧的结构不局限于行或列的维度,支持按单元格粒度加载和访问数据。目前在 HighTable 中,我们按整行加载,但未来可以进一步优化 —— 先计算可见列,再按需懒加载列数据。如果你对此功能感兴趣,欢迎参与 相关讨论。

##### 懒加载的效果

假设有 100 亿行数据,每行 100 字节,总数据量为 1TB。将其全部加载到内存中显然不现实,但借助懒加载,我们每次只需加载约 3KB 的可见部分(大约 30 行),便能保持良好的性能。

数据懒加载是处理浏览器中大规模数据集的第一步。接下来要解决的问题是:避免一次性渲染过多的 HTML 元素。

#### 技术二:表格切片

在软件工程中,当你试图优化性能时,第一步就是砍掉那些徒劳无功的计算。在我们的场景中,如果表格有一百万行而同一时刻只能看到 30 行,为什么要渲染一百万个<tr>HTML 元素呢?作为参考,Chrome 建议创建或更新的 HTML 元素数量不超过 300 个,以确保最佳的响应速度。

<HighTable>组件中,只有可见的那一片表格会被渲染,其余行元素压根不存在。

为了实现这一点,需要调整 HTML 结构 —— 在视口和表格之间添加一个中间div元素,我们称之为画布(canvas): <div class="viewport" style="overflow-y: auto;">   <div class="canvas" style="position: relative; height: 30000px;">     <table class="table" style="position: absolute; top: 3000px;">       <!-- 表格仅渲染可见行 -->       ...     </table>   </div> </div> 本文后续内容(包括技术三、四、五)将沿用这一 HTML 结构。

> 这里的 canvas div 与 HTML 的<canvas>元素毫无关系。如果觉得容易混淆,欢迎提出更好的命名建议。

画布的高度设置为能容纳所有行的大小: canvas.style.height = ${data.numRows * rowHeight}px` 这样就能让视口的滚动条呈现出预期的大小。正如前面滚动基础部分所述,viewport.scrollHeight等于canvas.clientHeight

画布充当一个参照物,用于对表格切片进行绝对定位。

下方的交互组件展示了表格切片的工作原理。上下滚动左侧方框,观察右侧方框如何在只渲染可见行的同时模拟滚动效果。点击 "完整表格" 按钮,可以查看渲染出的行在完整表格中的位置:

!Image 9

交互组件:左侧为视口,右侧展示画布和表格切片

从右侧可以看到,只有可见行被渲染出来。表格切片包含 6 行而非 10 行(具体数量取决于滚动位置,也可能是 7 行)。

表格切片内部的 HTML 结构如下:

                  ...第 100 行...     ...第 101 行...     ...     ...第 119 行...         
假设数据有 1,000 行,每行高度为 30px,视口高度为 600px(因此大约同时可见 20 行)。如果用户向下滚动了 3,000px,实际的元素中只会渲染第 100 到 119 行。

> 上面的 HTML 是简化后的版本。在 HighTable 的实际实现中,我们会渲染表头,并在可见行的前后各添加一些填充行,以提升滚动体验。

表格的顶部偏移量需要调整,使其在完整表格中精确对齐(点击 "显示 / 隐藏" 按钮可查看完整表格的效果)。其值等于第一可见行在虚拟完整表格中的位置,近似等于viewport.scrollTop,但需减去首行顶部被遮挡的像素偏移。因此: table.style.top = ${   viewport.scrollTop - (viewport.scrollTop % rowHeight) }px; 这些计算在每次滚动事件时都会执行(以及其他任何变化时:视口高度改变、行数更新等)。计算完成后,表格切片会用新的可见行重新渲染,表格位置会更新为新的top值,并在需要时通知数据帧加载新的可见单元格数据。

有一个细节值得一提 —— 固定表头。在中,包含列名的表头作为表格元素的一部分渲染在

中,而非独立元素。这对可访问性大有裨益,因为屏幕阅读器可以轻松识别与每个数据单元格关联的表头单元格;对列宽调整也同样有利,因为表头和数据单元格由浏览器自动对齐。借助 CSS 属性position: sticky(参见 MDN 上的 sticky 文档),表头行在滚动时始终固定在视口顶部。我们在计算第一可见行时会将其纳入考量。 【第3051期】浏览器也拥有了原生的 “时间切片” 能力!

值得注意的是,表格切片技术并非纵向滚动的专属。同样的思路也可用于横向滚动(只渲染可见列)。不过这一需求相对没那么迫切,因为表格的列数通常远少于行数。如果你对虚拟列功能感兴趣,欢迎参与 相关讨论。

##### 表格切片的效果

假设有 100 亿行数据,而同一时刻可见 30 行,我们只需渲染 30 个 HTML 元素,而非 100 亿个。由于渲染的元素数量恒定不变,无论行数多少都能保持良好的性能。

到目前为止,一切都还算常规操作。接下来的技术则更具 HighTable 特色,旨在应对处理数十亿行数据时浮现的独特挑战。

#### 技术三:无限像素

技术二运作完美 —— 直到它失效。正如 Eric Meyer 在他的博文《Infinite Pixels》中所阐述的,HTML 元素存在最大高度限制,具体数值因浏览器而异。最受限的是 Firefox:约 1700 万像素。由于画布高度随行数线性增长,若行高为 33px(HighTable 的默认值),那么最多只能渲染 50 万行。

我们在 HighTable 中的解决思路是:为画布设定一个最大高度,超过此上限后对滚动条的分辨率进行降采样。在 HighTable 中,这个阈值设定为 800 万像素。

具体而言,超过阈值后,滚动一个像素对应完整表格中的多个像素。降采样系数等于完整表格的理论高度与画布最大高度之比。得益于这个系数,当你将滚动条拖到一半时,无论表格有多大,都恰好定位到表格正中间。

低于阈值时,降采样系数为 1,一切如前 —— 滚动一个像素对应完整表格中的一个像素。

降采样系数的计算方式如下: const fullTableHeight = data.numRows * rowHeight const maxCanvasHeight = 8_000_000 if (fullTableHeight <= maxCanvasHeight) {   downscaleFactor = 1 } else {   downscaleFactor =     (fullTableHeight - viewport.clientHeight) /     (maxCanvasHeight - viewport.clientHeight) } 此时,第一可见行的计算方式变为: firstVisibleRow = Math.floor(   (viewport.scrollTop * downscaleFactor) / rowHeight ) 而表格的顶部偏移量则设置为让第一可见行与视口顶部对齐: table.style.top = ${viewport.scrollTop}px; 这使得用户即使面对数十亿行数据,也能在整个表格中自如导航。

下方的交互组件展示了滚动条降采样的工作原理。上下滚动左侧方框,观察右侧方框如何模拟滚动效果,实现在百亿行数据中的导航。

!Image 10

交互组件:左侧为视口,右侧展示画布和表格

但这带来了一个弊端。原生滚动条的精度极限是 1 个物理像素。在 "高分辨率" 屏幕上,表观精度为一个 CSS 像素的几分之一(1 /devicePixelRatio)。但为简化讨论,我们以一个像素为单位。

说一个有趣的细节:通过编程方式设置滚动值的结果其实难以预测。它取决于设备像素比(devicePixelRatio),而设备像素比又受缩放级别以及其他因素的影响。例如,element.scrollTo({top: 100})的实际结果可能是scrollTop = 100scrollTop = 100.23,或scrollTop = 99.89。你无法精确预知,只能确保在一个像素的误差范围内。 scrollTop的值甚至可能超出预期范围,比如出现负数或大于最大值scrollHeight - clientHeight的情况。为了防止这类浏览器特有的过度滚动效应,HighTable 在响应滚动事件时,始终将scrollTop值钳制在预期范围内,并应用 CSS 规则overflow-y: clip。使用clip而非hidden,可以让固定表头依然可见 —— 虽然说实话,我也不太确定其中的原理。

因此,当降采样系数很大时(如上例中的 2,189,781,021),最小的一次滚动(1px)对应完整表格中的 2,189,781,021 个像素。按行高 30px 计算,最小一次滚动约对应 72,992,701 行。这就在可达行之间制造了间隙:

* 当viewport.scrollTop = 0时,可见行为第 0 到第 5 行

* 当viewport.scrollTop = 1时,可见行为第 72,992,700 到第 72,992,705 行

* 当viewport.scrollTop = 2时,可见行为第 145,985,401 到第 145,985,406 行

* 以此类推……

比如第 6 到第 10 行就无法导航到。将viewport.scrollTop设为 0.00000000274 来定位到第 6 至第 10 行是不可能的,因为浏览器会将滚动位置四舍五入到最近的整数像素。

##### 无限像素的效果

假设有 100 亿行数据,无限像素技术使得用户能够在整个行跨度内自由导航。行数没有上限,因为我们总可以通过增大降采样系数来适配画布的最大高度。

但由于滚动条精度的限制,若行高为 30px、画布为 800 万像素,每滚动一个像素就会使表格移动 1,250 行。这意味着每 1,250 行中只有一行(及其相邻行)是可达的。

因此,无限像素技术提供了在数十亿行间全局导航的能力,但无法实现精细滚动,且部分行不可达。技术四将解决这一问题。 【第2801期】在技术与产品设计的合作中寻求像素完美

#### 技术四:像素级精准滚动

上一项技术实现了全局滚动,但阻碍了用户的局部滚动 —— 因为任何滚动操作都会跳过大段不可达行的间隙。

为此,我们实现了两种滚动模式:局部滚动和全局滚动。局部滚动意味着逐像素移动表格切片(精度甚至高于逐行滚动),而全局滚动则意味着直接跳转到滚动条指示的位置。

这套逻辑需要一个包含三个值的状态:{ scrollTop, globalAnchor, localOffset }

* 上次视口滚动位置存储在状态中,用于在每次滚动事件时计算滚动增量。

* 全局锚点是上次全局滚动时对应的视口 scrollTop 值。它在每次全局滚动时更新,但局部滚动时保持不变。

* 局部偏移量是在全局锚点基础上叠加的偏移,用于计算当前滚动位置。它在每次局部滚动时更新,全局滚动时重置为 0。

第一可见行通过全局锚点和局部偏移量共同计算得出: const firstVisibleRow = Math.floor((     state.globalAnchor * downscaleFactor + state.localOffset   ) / rowHeight) 表格的绝对定位现在变为: table.style.top = ${viewport.scrollTop + state.localOffset}px; 每次滚动事件发生时,我们计算滚动增量的大小(即新的视口 scrollTop 与状态中存储的上一次值之差),然后决定执行:

* 全局滚动 —— 当滚动增量较大时(典型场景是拖拽滚动条),直接跳转到新的全局位置(技术三);

* 局部滚动 —— 当滚动增量较小时,例如使用鼠标滚轮。此时,状态中的globalAnchor保持不变(即不再与实际的 scrollTop 值同步),通过调整localOffset使移动呈现为局部效果(例如,向下移动 3 行)。

用代码表示,逻辑大致如下(简化的伪代码): const state = getState() const delta = viewport.scrollTop - state.scrollTop if (Math.abs(delta) > localThreshold) {   // 全局滚动   state.localOffset = 0   state.globalAnchor = viewport.scrollTop } else {   // 局部滚动   state.localOffset += delta } setState(state) 现在,用户既可以在当前行附近自由浏览,也可以跳转到数据的任意位置。

下方的交互组件展示了双模式滚动的效果。上下滚动左侧方框,观察右侧方框如何模拟滚动效果,实现在百亿行数据中的局部和全局导航。

!Image 11

交互组件:左侧为视口,右侧展示画布和表格

通过这种方式,小幅滚动呈现为局部移动,大幅滚动则跳转到预期的全局位置。用户可以在整个表格中自由导航,触及每一行。无论使用鼠标滚轮、触控板、键盘(表格聚焦时)还是滚动条,用户的滚动体验都与浏览器原生行为一致。

##### 像素级精准滚动的效果

假设有 100 亿行数据,双模式滚动允许通过原生滚动条访问完整表格的任意像素位置。用户可以用鼠标滚轮进行局部滚动,也可以通过拖拽滚动条进行全局滚动。

这一方案在完整表格高度不超过最大画布高度(HighTable 中为 800 万像素)的平方时有效,对应约 64 万亿像素。因此,在行高为 30px 的条件下,最多可为 2 万亿行数据保证 1px 的精度。

超过这个上限后,最小步进将大于 1px,但在 64 万亿行以内每一行仍然可达!再往上,部分行才会变得不可达。

最后一个挑战是以编程方式跳转到任意单元格(即对表格任意位置的随机访问),无论是通过键盘操作还是 "跳转到指定行" 的输入框,都不必关心当前处于局部还是全局滚动模式。随机访问需要将纵向滚动和横向滚动解耦,我们将在下一节中详细说明。

#### 技术五:两步随机访问

HighTable 的需求之一是支持键盘导航(例如按 ↓ 跳转到下一行)。幸运的是,Web 无障碍倡议(WAI)通过网格模式和数据网格示例提供了相关指导。我们使用 tabindex 漫游机制来管理焦点,实现了所有预期的键盘交互。

浏览器在调用cell.focus()时提供了一个便利的默认行为:自动滚动到该单元格并聚焦。但在 HighTable 中,我们没有采用这个默认行为,因为它会将单元格定位到视口正中央,体验并不自然。

为了获得理想的效果,我们首先通过调用cell.scrollIntoView({block: 'nearest', inline: 'nearest'})以最小的滚动量来显示目标行和列,然后通过cell.focus({preventScroll: true})在不触发滚动的情况下设置焦点。

遗憾的是,WAI 资源中介绍的键盘导航技术是针对完整表格设计的。而由于技术二(表格切片)、技术三(无限像素)和技术四(像素级精准滚动)的存在,实际操作需要多个步骤。特别是,为了让用户能通过键盘移动活动单元格,我们需要将纵向滚动逻辑与横向滚动逻辑分离。

当用户移动活动单元格时,目标位置可以在表格的任何地方:↓ 移动到下一行,而 Ctrl+↓ 则直接跳转到最后一行。如果移动跨度很大,我们可能需要先进行纵向滚动,确保目标单元格存在于 DOM 中。

随机访问表格中的任意行时也会遇到同样的问题,例如嵌入的应用提供了 "跳转到指定行" 的功能。表格应当以编程方式滚动到目标行,并聚焦到目标列的单元格,而无需关心当前处于局部还是全局滚动模式,也无需在意横向滚动的位置。

具体流程如下:

* 1、计算新的状态(全局锚点和局部偏移量),使目标单元格所在的行进入可见范围;

* 2、如果全局锚点发生了变化,以编程方式滚动到新的 scrollTop 位置;

* 3、滚动完成后,重新渲染表格切片,确保目标单元格存在于 DOM 中;

* 4、如有需要,通过cell.scrollIntoView({inline: 'nearest'})进行横向滚动;

* 5、使用cell.focus({preventScroll: true})将焦点设置到新的单元格上。

需要注意的是,对于第 1 步(计算新状态),我们遵循block: 'nearest'的行为,尽量减少滚动量。如果目标行在当前视口下方,它将成为下一个视口中的最后一行可见行;如果在上方,则成为第一行可见行;如果已经可见,则不进行纵向滚动。

将纵向和横向滚动解耦的伪代码需要一个标志位,以防止在编程式纵向滚动期间执行横向滚动和聚焦操作: / 单元格导航代码中 / const shouldScroll = state.update() renderTableSlice() if (shouldScroll) {   // 设置标志位,防止编程式滚动期间   // 执行横向滚动和聚焦   setFlag('programmaticScroll')   viewport.scrollTo({top: state.globalAnchor, behavior: 'instant'}) } / 滚动事件处理函数中 / if (isFlagSet('programmaticScroll')) {   // 编程式滚动完成后,   // 允许横向滚动和聚焦   clearFlag('programmaticScroll') } / 单元格渲染代码中 / if (!isFlagSet('programmaticScroll')) {   // 允许横向滚动和聚焦   cell.scrollIntoView({inline: 'nearest'})   cell.focus({preventScroll: true}) } 我们在编程式滚动时设置behavior: 'instant',以确保只收到一个滚动事件。另一种选择behavior: 'smooth'`会触发多个滚动事件,导致标志位被过早清除,并因中间状态中意料之外的 scrollTop 值而与内部状态产生冲突(参见相关 issue)。

##### 两步随机访问的效果

得益于这项技术,即使面对数十亿行数据,用户也能通过键盘访问表格中的任意单元格,表格会自动滚动到预期位置。纵向和横向滚动彼此解耦,用户按 → 切换到下一列时不会触发纵向滚动,按 ↓ 时同样不会引发横向滚动。

#### 结语

无需伪造滚动条,无需用 canvas 元素渲染表格 —— 我们只依赖 Web 平台本身。凭借这五项基于原生 HTML 元素的技术,HighTable 让你在浏览器中即可流畅地浏览远程数据文件中的数十亿行数据,浑然天成。 https://github.com/hyparam/hightable

关于本文

译者:@飘飘

作者:Sylvain Lesage

原文:https://rednegra.net/blog/20260212-virtual-scroll/

!Image 12: 前端早读课 前端早读课 @前端早读课

One Sentence Summary

This article delves into how the React component HighTable overcomes browser limitations with five core technologies to achieve a virtual scrolling solution that smoothly displays and operates billions of rows of data on the web.

Summary

The article details the vertical scrolling technology stack employed by the HighTable component when handling massive datasets (billions of rows). The core challenges lie in browser memory limitations, DOM rendering pressure, and the maximum height limit of native HTML elements (e.g., Firefox's 17 million pixels). The author proposes five key technologies: 1. Lazy loading data frames, fetching visible rows on demand; 2. Table slicing, rendering only DOM nodes within the viewport; 3. Infinite pixel downsampling, breaking through browser height limits via mathematical mapping; 4. Pixel-level precise scrolling, compensating for precision loss due to downsampling using a "local + global" dual-mode logic; 5. Two-step random access, enabling keyboard navigation and programmatic jumps for extreme data volumes. This solution is entirely based on native HTML elements, requires no Canvas rendering, and balances performance with accessibility.

Main Points

* 1. Implement on-demand lazy loading and caching mechanisms through DataFrames.

Design asynchronous fetch and synchronous getCell interfaces to load only the cell data required for the current viewport and cache it in memory, preventing massive data from overwhelming browser memory.

* 2. Utilize table slicing technology to maintain a constant number of DOM nodes.

Introduce a canvas layer between the viewport and the table, rendering only about 30 visible rows of data, ensuring rendering overhead remains low regardless of the total data volume.

* 3. Introduce a downscaleFactor to break through browser height limitations.

Addressing the browser's maximum height limit of approximately 17 million pixels, when the data volume is too large, the scrollbar displacement is proportionally mapped to a larger virtual space, enabling infinite row navigation.

* 4. Design a "local + global" dual-mode scrolling logic to solve the precision loss problem.

By judging the scroll increment size, it automatically switches between global jumps and local pixel offsets, ensuring users can both quickly navigate and perform fine-grained row-by-row scrolling.

* 5. Support complex keyboard interactions through two-step random access and scroll decoupling.

Separate vertical and horizontal scrolling logic, use flags to control the execution order of programmatic scrolling, ensuring pixel-level cell focusing can still be achieved across billions of rows of data.

Key Quotes

* Solutions that work well at a small scale become overwhelmed once data volume escalates. * Because canvas height grows linearly with the number of rows... it can render at most 500,000 rows. Our solution in HighTable is to downsample the scrollbar's resolution. * Local scrolling means moving the table slice pixel by pixel, while global scrolling means jumping directly to the position indicated by the scrollbar. * No need to fake scrollbars, no need to render tables with canvas elements — we rely solely on the Web platform itself. * This solution is effective when the full table height does not exceed the square of the maximum canvas height... guaranteeing 1px precision for up to 2 trillion rows of data.

AI Score

91

Website mp.weixin.qq.com

Published At Today

Length 7884 words (about 32 min)

Tags

Virtual Scrolling

React

Performance Optimization

Big Data Display

Browser Underlying Limitations

Related Articles

* [[Morning Brief] Who Is Your AI Workplace Partner? This Data Has the Answer](https://www.bestblogs.dev/en/article/628efe2b "A survey of tech professionals across various roles reveals AI tools are increasingly integral to the workplace, significantly boosting productivity. Entrepreneurs benefit the most, engineers favor specialized tools, and AI's future role is seen in deeper strategy and collaboration beyond mere task execution.") * AI-Assisted Frontend Animation Development animations to frontend code via the MCP toolchain and Cursor AI IDE.") * Episode 3641: How Strong Is AI at Writing React Code? Addy Osmani's Practical Guide * Redis Stream vs. Traditional MQ? A Comprehensive Guide to Queue Selection (Applicable Scenarios, Pros & Cons, and Practical Recommendations) as a message queue with professional message queues (Kafka/RabbitMQ), highlighting their pros and cons, and provides practical selection recommendations.") * 15 Key Trade-offs in System Design * In-depth Explanation of LLM Inference Acceleration Principles: Fractal Patterns and Resource Calculation Formulas * [[Issue 3636] Deep Dive: Practical Experience in Building High-Performance Web Applications with React + Next.js](https://www.bestblogs.dev/en/article/210b0fb9 "This article provides a deep analysis of how to build high-performance web applications using React and Next.js between 2022 and 2025 through multiple real-world cases, covering performance optimization, rendering strategies, caching, state management, DX, and UX.") * Modern Node.js Development Patterns for 2025 (Issue 3629) * Precise Dead Code Cleanup Through Code Coloring and Execution Analysis * [[Issue 3660] Stop Writing Tests for Coverage: This Is How Truly Reliable Unit Tests Should Be Done](https://www.bestblogs.dev/en/article/84834826 "This article explores how to build truly reliable unit tests by testing \"behavior\" rather than \"implementation,\" emphasizing pragmatic methods like mocking system boundaries, using in-memory databases, and HTTP recording to boost deployment confidence.") HomeArticlesPodcastsVideosTweets

[Episode 3671] Virtual Scrolling for Billions of Rows of ...

查看原文 → 發佈: 2026-03-17 09:02:00 收錄: 2026-03-17 14:01:21

🤖 問 AI

針對這篇文章提問,AI 會根據文章內容回答。按 Ctrl+Enter 送出。