前端大数据量渲染实战 2026:虚拟列表与万行表格的性能工程
列表渲染到几千行,页面开始卡顿;表格撑到上万行,滚动直接掉帧——DOM 的成本从来不是线性的,而大多数性能问题的答案只有一个:只渲染用户看得见的部分。本文系统讲解大数据量渲染的完整工程:从 DOM 成本的真相、分页与无限滚动的适用边界,到虚拟列表的核心原理与最小实现、动态高度的测量与预估修正、虚拟化表格的行列双虚拟化,再到 React 生态选型、交互细节与可访问性,最后给出性能测量方法与落地清单。
今日技术简讯
📰 技术简讯 · 2026-10-16
今日聚合 6 条热门技术内容(中文素材优先)。
🤖 AI / LLM
1. 商务部拟对 AI 相关技术实施出口管制,修订禁止出口限制出口技术目录
- 链接:https://m.bjnews.com.cn/detail/1792137269129878.html
- 来源:新京报
- 摘要:商务部就修订《中国禁止出口限制出口技术目录》答记者问,其中涉及对人工智能相关技术的出口限制。这将直接影响跨境模型合作、开源发布与国内团队的合规策略,开发者需关注最终目录对模型权重、训练框架出口的界定。
2. Reflection AI 开源 501B 参数 MoE 模型 Beam(Apache 2.0)
- 链接:https://reflection.ai/blog/beam-is-available
- 来源:Reflection AI 官方博客
- 摘要:Reflection AI 发布 501B 参数、MoE 架构的开源模型 Beam,采用 Apache 2.0 许可,权重已上架 Hugging Face(含量化版本),宣称可自托管、定制与横向扩展。这是开源模型阵营对闭源的重要回应,关注本地部署与微调的开发者值得实测。
🎨 前端 / Web
3. Expo SDK 58 正式发布
- 链接:https://expo.dev/changelog/sdk-58
- 来源:Expo 官方
- 摘要:Expo SDK 58 上线,跟进 React Native 新架构默认化路线,更新 expo-router、EAS 相关能力与依赖基线。移动端 React 开发者升级前需按官方升级指南同步调整原生依赖,是本周移动前端的必看变更。
4. VS Code 界面现代化改造
- 链接:https://code.visualstudio.com/blogs/2026/10/15/modern-ui
- 来源:Visual Studio Code 官方博客
- 摘要:VS Code 团队介绍对工作台 UI 的现代化改造,涵盖视觉风格、布局细节与渲染路径的更新,兼顾性能与美观。对基于 Web 技术栈构建桌面端应用的团队,其界面演进思路有直接借鉴价值。
⚙️ 后端 / 架构
5. Postgres 19 延期:AI 审查者发现的 bug 多到人类修不完
- 链接:https://www.theregister.com/databases/2026/10/16/postgres-19-delayed-after-ai-reviewers-find-more-bugs-than-humans-can-fix/5303471/
- 来源:The Register
- 摘要:Postgres 19 因 AI 辅助代码审查发现大量新 bug、超出人类修复能力而宣布延期。这是 AI 参与开源核心项目质量流程的典型案例:AI 时代"发现容易修复难"正在成为新瓶颈。
🚀 独立开发 / OPC
6. 在 AI SaaS 垃圾泛滥时代,用 offline-first 应用赢得用户信任
- 链接:https://blog.geckocode.pl/2026/10/16/privacy-first-apps.html
- 来源:Gecko Code(独立开发者博客)
- 摘要:独立开发者撰文阐述 offline-first + 本地优先架构如何成为独立产品对抗云端 SaaS 的差异化卖点:数据留在本地、断网可用、天然隐私合规。对一人公司是切合当前用户情绪的选型与营销思路。
数据来源:掘金 / InfoQ 中文 / 即刻 / 少数派 / HN 采集日期:2026-10-16 (UTC+8)
今日深度文
前端大数据量渲染实战 2026:虚拟列表与万行表格的性能工程
"列表有一万条数据,页面卡死了。"这是前端性能工单里最常见的句式之一。有意思的是,用户的屏幕上同时显示的永远只有几十行——也就是说,一万条数据里,绝大多数 DOM 节点从头到尾没有被真正"看过",却实打实地占着内存、参与着布局计算。虚拟列表(virtualization)解决的就是这个浪费:只渲染可视区域附近的元素,用极小的 DOM 维护一个"看起来完整"的长列表。这篇文章把这个看似简单的技术讲透——因为它在简单和复杂之间,藏着好几层需要认真对待的细节。
一、先算账:DOM 的成本到底在哪
很多优化文章一上来就给方案,但理解成本模型才能做对取舍。浏览器渲染一万行列表,开销分布在四个层面:
大数据量列表的四层成本:
① 内存:每个 DOM 节点在 JS 堆与渲染树中都占内存,
一万个列表项(每项十来个节点)就是十几万节点,
内存占用可能达到数百 MB 量级。
② 布局/绘制:首次插入需要完整 layout + paint;
更关键的是后续任何影响布局的改动
(插入、删除、动态高度变化)都会触发重排。
③ 事件与响应式开销:现代框架给每个组件实例
附加响应式依赖、事件监听、生命周期——
框架的成本往往高于 DOM 本身。
④ 样式匹配:选择器复杂时,新增节点的样式计算
成本随 DOM 规模上升。
关键结论:**问题不在"数据多",而在"渲染了不需要渲染的东西"。**所以解法的第一步永远不是换更快的框架,而是砍掉不可见的渲染。
二、先做减法:什么时候根本不需要虚拟列表
虚拟列表不是免费午餐——它牺牲了浏览器原生的滚动行为(锚点、Ctrl+F 查找、无障碍树完整性),引入了自己的复杂度。所以先问一句:真的需要吗?
决策顺序(从简到繁):
① 数据量小(百条以内):直接渲染,别折腾。
② 中等量且用户按页消费:传统分页,简单可靠,
SEO 也友好。
③ 信息流类、用户持续下滑:无限滚动(滚动加载下一页),
每次只往 DOM 里追加一页。
注意:无限滚动不停追加,DOM 依然会无限膨胀,
长会话下照样卡——只是把卡顿推迟了。
④ 真正的巨量数据 + 必须一屏滚到底:虚拟列表。
一个常见的组合拳是"无限滚动 + 虚拟列表":数据按页取,DOM 按可视区渲染,两头都可控。这篇文章的主角是第四种与这个组合。
三、核心原理:三行话讲完虚拟列表
虚拟列表的本质,是一个"滚动驱动的取景器":
虚拟列表三要素:
① 撑起高度:外层容器里放一个"总高占位元素",
高度 = 全部条目的预估总高度,
让滚动条的长度和位置与完整列表一致。
② 计算窗口:监听滚动,用 scrollTop 除以行高,
算出当前可视区起点是第几条(startIndex),
再加上可视区能容纳的条数得到 endIndex。
③ 平移渲染:只把 [startIndex, endIndex] 的条目
渲染到一个内层容器,并用 transform: translateY()
把它平移到正确的位置,视觉上与完整列表无异。
再加一个工程细节:窗口要向上下各多渲染几条(overscan),否则快速滚动时边缘会露出"还没来得及渲染"的空白。
四、动手实现:固定高度的虚拟列表
固定行高是虚拟列表的最简形态,理解它再看动态高度。完整实现不到一百行:
/**
* 固定行高虚拟列表(React 实现)
* 原理:总高占位 + 滚动偏移计算窗口 + transform 平移
*/
function FixedSizeList({
items, rowHeight, height, overscan = 5, renderItem,
}: {
items: Item[]; // 全量数据
rowHeight: number; // 固定行高(像素)
height: number; // 容器可视高度
overscan?: number; // 上下额外渲染的条数
renderItem: (item: Item, index: number) => React.ReactNode;
}) {
const [scrollTop, setScrollTop] = useState(0);
/** 滚动时记录最新偏移;passive 监听避免阻塞合成线程 */
const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
};
// 可视区起点与终点(含 overscan 缓冲)
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const visibleCount = Math.ceil(height / rowHeight);
const endIndex = Math.min(items.length - 1, startIndex + visibleCount + overscan * 2);
// 窗口内条目整体向下平移到绝对位置
const offsetY = startIndex * rowHeight;
return (
<div style={{ overflowY: "auto", height }} onScroll={onScroll}>
{/* 总高占位:让滚动条与完整列表等长 */}
<div style={{ height: items.length * rowHeight, position: "relative" }}>
<div style={{ position: "absolute", top: 0, transform: `translateY(${offsetY}px)` }}>
{items.slice(startIndex, endIndex + 1).map((item, i) => (
<div key={startIndex + i} style={{ height: rowHeight }}>
{renderItem(item, startIndex + i)}
</div>
))}
</div>
</div>
</div>
);
}
三个容易踩的细节,直接决定手感:
实现细节:
① 用 transform 而不是 top 定位:transform 不触发重排,
由合成线程处理,滚动流畅度天差地别。
② key 必须用数据 ID 而不是窗口内索引:
窗口滑动时,同一索引对应的数据在变化,
用索引做 key 会导致组件复用错乱、状态串位。
③ onScroll 的 setState 频率很高:
React 18+ 的自动批处理已经缓解大半,
若仍有压力可配合 requestAnimationFrame 节流,
保证一帧至多计算一次窗口。
五、动态高度:虚拟列表的真正难点
真实世界的列表项高度往往不等——聊天气泡、动态卡片、可折叠行。这时"scrollTop 除以行高"的公式失效了,因为你还不知道每一行多高。
主流解法是**"预估 + 测量 + 修正"**三段式:
动态高度的工程方案:
① 预估:给每行一个预估高度(estimatedRowHeight),
用前缀和数组估算每一行的位置。
→ 滚动条长度一开始只是"近似对"的。
② 测量:每个行渲染后用 ResizeObserver 测量真实高度,
写回高度表。
③ 修正:测量会改变前缀和 → 已滚过的位置会漂移。
关键技巧:只修正"当前视口以下"的预估,
或者记录偏移差值补偿 scrollTop,
保证用户当前看到的内容不跳动。
第三步是所有虚拟列表库的功力所在。简化版的位置修正逻辑如下:
/**
* 动态高度列表的位置簿记
* 用预估高度建立前缀和,测量后按差值补偿滚动位置
*/
class DynamicHeights {
private heights: number[]; // 实测高度(未测量的行为预估值)
private offsets: number[]; // 前缀和:offsets[i] = 第 i 行的顶部位置
/**
* @param estimatedRowHeight 初始预估行高
* @param count 总条数
*/
constructor(private estimatedRowHeight: number, count: number) {
this.heights = new Array(count).fill(estimatedRowHeight);
this.rebuildOffsets();
}
/** 依据高度表重建位置前缀和 */
private rebuildOffsets(): void {
this.offsets = [0];
for (let i = 0; i < this.heights.length; i++) {
this.offsets[i + 1] = this.offsets[i] + this.heights[i];
}
}
/**
* 渲染后回写实测高度
* @param index 行号
* @param measuredHeight 实测像素高度
* @param currentScrollTop 当前滚动位置,用于差值补偿
* @returns 修正后的 scrollTop(若该行在视口上方)
*/
measure(index: number, measuredHeight: number, currentScrollTop: number): number {
const delta = measuredHeight - this.heights[index];
if (delta === 0) return currentScrollTop;
this.heights[index] = measuredHeight;
this.rebuildOffsets();
// 若被测行在当前视口上方,其高度变化会把内容"推走",
// 必须补偿 scrollTop,否则用户看到的画面会跳动
if (this.offsets[index] < currentScrollTop) {
return currentScrollTop + delta;
}
return currentScrollTop;
}
/** 二分查找:滚动位置对应的第一条可见行号 */
findStartIndex(scrollTop: number): number {
let lo = 0, hi = this.offsets.length - 1;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (this.offsets[mid] < scrollTop) lo = mid + 1;
else hi = mid;
}
return lo;
}
}
**预估高度越准,滚动条和内容跳变越少。**实践中一个有效的技巧:用首屏实际测量出的平均行高作为预估,而不是拍脑袋的固定值。
六、虚拟化表格:行列双向虚拟化
表格比列表多一个维度:列也可能很多(上百列的运营后台报表并不罕见)。完整方案是二维虚拟化——行和列都只渲染窗口:
二维虚拟化的思路:
行方向:与列表完全一致(scrollTop → 行窗口)
列方向:横向 scrollLeft → 列窗口,
总宽占位 + translateX 平移
合起来:只渲染"可视区矩形"内的单元格。
1 万行 × 100 列 = 百万单元格,
实际渲染的可能只有 20 行 × 15 列 ≈ 300 个。
表格特有的两个难题及对策:
/**
* 虚拟表格的两个关键 CSS 技巧
*/
// ① sticky 表头:行虚拟化时表头要钉住,
// 且与行窗口共享同一个横向滚动偏移
// <thead style={{ position: "sticky", top: 0 }}>
// ② sticky 首列:列虚拟化时用 position: sticky + left
// 固定关键列(如名称列),滚动横向时保持可见
// <td style={{ position: "sticky", left: 0, zIndex: 1 }}>
/**
* 横向列窗口的计算与纵向同理:
* 用列宽前缀和数组 + scrollLeft 二分求起止列
*/
function findColumnRange(prefixWidths: number[], scrollLeft: number, viewportWidth: number) {
// start:第一个右边缘越过 scrollLeft 的列
let start = 0;
while (prefixWidths[start + 1] <= scrollLeft) start++;
// end:第一个右边缘覆盖到可视区右缘的列
let end = start;
const rightEdge = scrollLeft + viewportWidth;
while (prefixWidths[end + 1] < rightEdge && end < prefixWidths.length - 2) end++;
return { start, end };
}
表格虚拟化的另一个现实建议:如果列多但行不算极端多(几千行),可以只虚拟化行、正常渲染列——复杂度减半,效果依然显著。按瓶颈所在决定虚拟化的维度,别为了技术完整而过度工程。
七、React 生态选型:三个主流库怎么选
自己实现适合理解原理和小型场景,生产项目通常直接选库:
React 虚拟化库选型参考:
① @tanstack/react-virtual(原 react-virtual v3)
框架无关内核 + React 绑定;动态高度支持成熟,
API 灵活(列表/表格/网格通吃),维护活跃。
→ 新项目的默认首选。
② react-window
react-virtualized 的轻量重构,固定/可变尺寸列表
开箱即用,体积小、心智负担低。
→ 需求标准、追求简单时的稳妥选择。
③ react-virtuoso
开箱即用程度最高:反向滚动(聊天室)、
分组列表、置顶行、自动恢复滚动位置等
特殊场景都有内建方案。
→ 聊天流、动态内容多、不想自己处理簿记时的省心之选。
选型口径:标准场景 TanStack Virtual;
聊天室/复杂滚动行为选 Virtuoso;
维护老项目遇到 react-window 也无需强行迁移。
八、交互与可访问性:虚拟列表的代价要还
虚拟列表砍掉的 DOM 也带走了一些浏览器原生能力,需要主动补回来:
必须处理的交互细节:
① 键盘导航:滚动容器要能响应方向键/PageUp/PageDown,
并同步更新焦点与选中项(虚拟列表不拦,
但焦点元素被卸载时会丢,需要手动恢复)。
② 锚点跳转:定位到第 N 条(如通知列表跳到某条),
固定高度直接 scrollTop = N * rowHeight;
动态高度要依赖前缀和——这正是保留 offsets 数组的
另一个理由。
③ 查找与无障碍:Ctrl+F 找不到未渲染的内容是虚拟列表
的固有代价。重要文本场景可考虑"渲染层虚拟 +
数据层完整"的折中,或为屏幕阅读器提供
aria-live 的摘要信息。role="list" 与
aria-rowcount 声明总行数也有帮助。
④ 滚动位置恢复:列表页跳详情再返回,
要恢复原滚动位置(连同窗口计算状态),
否则"返回列表回到顶部"会被用户视为故障。
九、测量:先有数字,再谈优化
性能优化的纪律是:**先测量,再动手,改完再测。**长列表场景的三个核心指标:
/**
* 用 PerformanceObserver 监控长任务
* 长任务(>50ms)是滚动掉帧的直接元凶
*/
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 超过一帧预算(16.7ms)的渲染工作都值得追查
if (entry.duration > 50) {
console.warn("long task:", Math.round(entry.duration), "ms");
}
}
});
observer.observe({ entryTypes: ["longtask"] });
测量清单:
□ Chrome DevTools Performance 面板录一次滚动:
看掉帧、Long Tasks、Layout/Recalculate Style 占比。
□ React 项目打开 React DevTools Profiler:
确认滚动时只有窗口内组件重渲染,
父组件不要因 scrollTop 状态变化全树重渲。
□ 内存:Memory 面板对比虚拟化前后堆大小。
□ 真机验证:低端 Android 设备的体感
才是"能不能用"的最终标准。
十、落地清单
方案确认
□ 数据量与用户消费方式评估:分页/无限滚动/虚拟列表
□ 行高固定还是动态,是否需要列虚拟化
□ 选型:TanStack Virtual / react-window / Virtuoso
实现要点
□ key 用数据 ID,绝不用窗口内索引
□ transform 平移窗口,不用 top/left
□ 动态高度:预估 + ResizeObserver 实测 + 滚动补偿
□ overscan 合理设置(过大白渲染,过小露白)
□ 锚点跳转、键盘导航、滚动位置恢复
质量保障
□ Performance 面板验证无长任务、滚动满帧
□ 大数据量(万级)真机过一遍快速滚动
□ 屏幕阅读器与 aria 属性检查
结语:虚拟列表是一种"诚实的渲染"
虚拟列表的技术含量,不在于那三行核心原理,而在于把"看起来一样"的细节做到位:滚动条长度、位置补偿、键盘与读屏、状态恢复。它代表的其实是一种更诚实的渲染观——只把用户看得见的东西渲染出来,但让整个系统的行为与"全量渲染"无法区分。
当你在某个仪表盘里把十万行表格从"必卡"调到"满帧",你会发现这类优化的方法论可以迁移到几乎所有前端性能问题:先找到"渲染了不需要渲染的东西",再想办法不渲染它。性能工程的大多数胜利,靠的不是更快的机器和框架,而是更少的浪费。
�� 同主题文章
View Transitions API 实战 2026:用浏览器原生能力做流畅的页面过渡
从列表点击进入详情,图片像被一只无形的手放大、平移、落位;返回时它又缩回列表原位。这种过去要依赖重量级动画库、还要手动对齐位置的共享元素过渡,如今浏览器原生就能完成。2026 年,View Transitions API 已在所有主流浏览器全面可用,跨文档过渡更被纳入 Interop 2026 互操作性计划。本文系统实战 View Transitions:单页应用同文档过渡的基本原理、view-transition-name 命名与共享元素、自定义关键帧动画、列表到详情的经典场景、多页应用跨文档过渡与 pageswap/pagereveal 事件、React/Vue 框架集成、无障碍与降级,以及性能边界。
前端状态管理完整实战 2026:从 Redux 到 Signals 再到 Server State
前端状态管理是 React 生态永恒的话题。从 Class 时代的 Redux 到 Hooks 时代的 Context + useReducer,再到 2024 年 Signals 的兴起和 2025 年 Server State 的深度整合,状态管理的范式在持续演进。核心矛盾从未改变:状态在哪里放、怎么同步、组件怎么订阅。本文完整实战 2026 年前端状态管理:状态分类(UI / 服务端 / 全局)、Redux 到 Zustand 的简化路线、Signals 细粒度响应式的原理与迁移、Server State 与本地状态的边界、缓存策略与乐观更新、以及独立开发者的选型决策树。
React Server Components 完整实战 2026:服务端渲染、流式与数据获取的架构重组
React Server Components(RSC)是 React 诞生以来最深刻的架构变革:组件被分为「服务端组件」与「客户端组件」两种,前者只在服务端运行、不占用浏览器 JavaScript 体积、可直接访问数据库,后者保留交互能力。它不是 SSR 的升级版,而是对 React 渲染模型的重新设计。本文完整实战 RSC:服务端组件与客户端组件的分工、数据获取的革命、流式渲染与 Suspense、缓存策略、与客户端状态管理的边界、迁移路径与常见误区。