前言
做前端架构的人要不要懂运行时性能?我的回答是 要——不是去背 Web Vitals 阈值,而是建立”列表 10000 行怎么不卡”的工程判断。原因有三:
- 简历里 FinUI 大表格性能优化是真实案例。300 列 × 500 行的表格,虚拟滚动前后差距 800ms → 50ms(16 倍)。
- 运行时性能是”看不见的 bug”。LCP 优秀不代表 INP 优秀,Web Vitals 三件套都要盯。
- 架构师必须会选虚拟化方案。react-window / vue-virtual-scroller / TanStack Virtual / 自实现,决策直接影响 80% 长列表场景。
这一篇是『前端架构修仙路』的第 16 篇。我跳过具体库的 API(不讲 react-window 怎么用),用架构图 + 真实性能数据 + 选型表,把虚拟滚动原理 / Web Vitals 长尾 / 大表格调优讲透。下一篇深度聊 Electron 实战。
一、虚拟滚动:长列表的杀手锏
1.1 问题本质
10000 行 DOM = 浏览器渲染灾难:
- 每个 DOM 节点都要 layout / paint / compositing
- 10000 行 × 80px 高 = 800000px 文档高度
- 主线程被 layout 卡死,滚动掉帧到 5fps 以下
1.2 虚拟滚动原理
核心三件套:
- 可视区域窗口:固定高度的 scroll container
- 总高度撑出滚动条:用 padding-top / padding-bottom 模拟”10000 行的总高度”
- 只渲染当前可见的 DOM:scroll 事件触发 → 计算 visible range → 渲染该区间的数据
1.3 真实性能
10000 行表格:
普通渲染: 8s 首屏 + 5fps 滚动
虚拟滚动: 50ms 首屏 + 60fps 滚动
顺丰 ERP FinSpread 表格:300 列 × 500 行,原生 <table> 渲染 800ms,上虚拟滚动后 50ms(16 倍提速)。
二、虚拟滚动库选型
| 库 | 大小 | 特点 | 适用场景 |
|---|---|---|---|
| react-window | 6KB | 经典、API 简单 | 通用列表 / 网格 |
| react-virtuoso | 30KB | 自动 resize / 动态高度 | 高度不固定的内容 |
| TanStack Virtual | 15KB | 框架无关、TS 友好 | React / Vue / Solid |
| @tanstack/react-virtual | 同上 | 同上,React 专版 | 大表格场景 |
| vue-virtual-scroller | 14KB | Vue 1.x 时代的虚拟滚动 | Vue 2 / Vue 3 |
| @vueuse/core | 内置 useVirtualList | Vue 3 composable | Vue 3 + TS |
架构师决策:
- 固定高度列表:react-window(轻量、足够)
- 动态高度(聊天 / 社交):react-virtuoso(自动处理)
- 框架无关:TanStack Virtual
- 大型数据表格:TanStack Table + Virtual(组合拳)
三、Web Vitals 长尾优化:LCP 之外的三件事
3.1 LCP / INP / CLS 三件套
// web-vitals 上报全部三个指标
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
onLCP(metric => sendToAnalytics({ name: 'LCP', value: metric.value }));
onINP(metric => sendToAnalytics({ name: 'INP', value: metric.value }));
onCLS(metric => sendToAnalytics({ name: 'CLS', value: metric.value }));
3.2 INP 优化:交互响应延迟
INP(Interaction to Next Paint) = 用户交互到下一帧绘制的时间。
// ❌ 反模式:同步计算卡主线程
button.onclick = () => {
heavyCalculation(10000); // 卡 200ms
};
// ✅ 推荐:requestIdleCallback + Web Worker
button.onclick = () => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => heavyCalculation(10000));
} else {
setTimeout(() => heavyCalculation(10000), 0);
}
};
生产案例:FinUI 表头排序用 requestIdleCallback,INP 从 180ms 降到 50ms(3.6 倍)。
3.3 CLS 优化:避免布局抖动
/* ❌ 罪魁:异步加载的图片没设宽高 → 加载完撑开页面 */
img { width: auto; height: auto; }
/* ✅ 修复:始终声明尺寸 */
img {
width: 100%;
height: 300px; /* 预留位置 */
object-fit: cover;
}
/* 字体加载:font-display: swap 防止 FOIT 撑开 */
@font-face { font-display: swap; }
/* 异步插入 DOM 用 contain 隔离 */
.ad {
contain: layout; /* 告诉浏览器:ad 区域是独立的 */
}
生产案例:新闻列表加 aspect-ratio: 16/9 给图片容器,CLS 从 0.18 降到 0.02。
四、大表格(FinUI)实战调优
4.1 渲染瓶颈分析
FinUI 表格 300 列 × 500 行 = 150000 cells:
- DOM 节点: 150000 个 (1MB+ HTML)
- 首屏 layout: 800ms
- 滚动: 5fps
- 内存: 250MB
4.2 五层优化
// 1. 虚拟滚动(最大收益)
import { useVirtualizer } from '@tanstack/react-virtual';
// 2. 列虚拟化(300 列只渲染视口可见的)
import { useDynamicRowVirtualizer, useDynamicColumnVirtualizer } from '@tanstack/react-virtual';
// 3. Cell memo 避免无关 re-render
const Cell = React.memo(({ row, column }) => {
return <td>{row[column.key]}</td>;
}, (prev, next) => prev.row.id === next.row.id && prev.column === next.column);
// 4. 横向滚动同步
const [scrollLeft, setScrollLeft] = useState(0);
// 横向固定列 + 横向滚动同步到表头
// 5. 数据分片加载(500 行分 5 次请求)
const { data, fetchMore } = useInfiniteScroll({
totalCount: 100000,
loadMore: (offset) => api.getRows(offset, 100),
});
4.3 优化前后对比
FinUI 表格:
优化前: 首屏 800ms / 滚动 5fps / 内存 250MB
优化后: 首屏 50ms / 滚动 60fps / 内存 80MB
提速: 16× / 12× / 3×
五、长列表场景的其他方案
| 方案 | 适用场景 | 性能 |
|---|---|---|
| 虚拟滚动 | 1000+ 行可滚动 | ⭐⭐⭐⭐⭐ |
| 分页 | 数据不连续展示 | ⭐⭐⭐ |
| 无限滚动 | 社交信息流 | ⭐⭐⭐⭐ |
| windowing | 表格 + Grid | ⭐⭐⭐⭐⭐ |
| requestIdleCallback | 后台预处理数据 | ⭐⭐⭐ |
六、踩坑提醒(资深架构师请重点看)
- 不要让虚拟滚动包额外的 listener。每个 scroll 事件触发重渲染,框架外加 listener = 重复计算 2 次。
- 不要用 index 当 key。
<Cell key={i}>在排序 / 删除行时复用错误 DOM,INP 飙升。 - 不要忽视滚动条的边缘情况。macOS 隐藏滚动条 vs Windows 显示滚动条,视口高度差 17px,布局抖动。
- 不要混用虚拟滚动 + 复杂表头。固定列 + 横向滚动 + 复杂表头 = 状态管理灾难,用 TanStack Table 这种成熟方案。
- 不要忘记 SEO 长列表。搜索引擎只抓首屏内容,10000 行虚拟滚动的内容可能只索引前 20 行——关键内容用 SSR 渲染初始 HTML。
总结
这一篇用 5 个关键事实把运行时性能串起来:
- 虚拟滚动:可视窗口 + 总高度撑出滚动条 + 只渲染当前可见 DOM,10000 行从 800ms 降到 50ms。
- 库选型:固定高度 react-window / 动态高度 react-virtuoso / 框架无关 TanStack Virtual。
- Web Vitals 三件套:LCP + INP + CLS 都要盯,INP 用 requestIdleCallback 优化,CLS 用 aspect-ratio 预留。
- FinUI 大表格:虚拟滚动 + 列虚拟化 + Cell memo + 数据分片,16× 提速。
- 方案组合:虚拟滚动 + 分页 + 无限滚动 + windowing 各有适用场景,架构师按数据量选。
下一篇:Electron 实战——主进程 / 渲染进程 / USB Bridge / 柔记 PC 端项目实战经验。
5 道重点面试问题方向
Q1(答案):虚拟滚动的核心原理是什么?10000 行列表怎么做到 60fps 滚动?
A:虚拟滚动三大组件:① 可视区域窗口 —— 固定高度的 scroll container;② 总高度撑出滚动条 —— 用 padding-top / padding-bottom 模拟”10000 行总高度”;③ 只渲染当前可见 DOM —— scroll 事件触发 → 计算 visible range → 渲染该区间的数据。10000 行从 800ms 降到 50ms 的关键:DOM 节点从 100000+ 降到 ≈ 20 个,layout / paint / composite 的工作量都是 O(visible) 而不是 O(all)。面试要点:能说”虚拟滚动不是省了渲染,是延迟渲染 + 复用 DOM 池”。
Q2(思考):react-window / react-virtuoso / TanStack Virtual 三个虚拟滚动库怎么选?
Q3(思考):INP 和 CLS 是什么?分别怎么优化?
Q4(思考):300 列 × 500 行的企业级表格怎么优化?你具体做了哪些层?
Q5(思考):你在生产环境里是怎么定位「滚动卡顿」问题的?Chrome DevTools Performance 里看哪些指标?
💡 每道题后面都有 AI 助手按钮,可以一键拿到详细答案。
参考资料
- web.dev: Optimize INP (Google Developers) — INP 优化官方指南
- web.dev: Optimize CLS — CLS 优化官方指南
- TanStack Virtual (GitHub) — TanStack Virtual 虚拟滚动
- react-window (GitHub) — react-window 经典虚拟滚动
- Chrome DevTools Rendering Performance (Google Developers) — 运行时性能分析
- Web Vitals (GitHub) — 官方 Web Vitals 库