跳到主要内容

3 篇博文 含有标签「性能优化」

查看所有标签

前端首屏优化探索与方案:从指标到落地,大厂是怎么做到"秒开"的

· 阅读需 17 分钟

"首屏优化"是前端工程化里最常被问、也最难答好的话题——因为它不是一个技巧,而是一整套从指标定义 → 网络传输 → 资源加载 → 构建产物 → 渲染执行全链路的方法论。本文把这条链路完整走一遍:先讲清楚"快"到底指什么、慢在哪,再按层给出可落地的方案,最后用淘宝、美团、字节等大厂的真实实践收尾。


1. 为什么要优化首屏

首屏慢的代价是实打实的商业数字。美团技术团队在《前端黑科技:美团网页首帧优化实践》里引用过一组业界数据:

  • 57% 的用户更在乎网页在 3 秒内完成加载;
  • 52% 的在线用户认为网页打开速度影响其对网站的忠诚度;
  • 每慢 1 秒,页面 PV 降低 11%、用户满意度下降 16%
  • 半数移动用户因 10 秒内没打开页面而放弃。

所以"秒开"不是一个技术执念,而是直接关系收入指标(转化率、跳出率、留存)。这也是为什么大厂会把首屏性能做成性能预算(Performance Budget)——把它当"依赖"一样锁进 CI,超了就不让发版。

虚拟列表大厂方案调研:各场景的选型、量级判断与 fallback 策略

· 阅读需 14 分钟

一万条订单直接 map 渲染,浏览器会创建一万多个 DOM 节点——滚动掉帧、内存暴涨,甚至直接卡死。虚拟列表(Virtual List / Windowed Rendering)就是为了解决这个问题:不管数据有多少,始终只渲染视口附近的那几十个节点。但"上虚拟列表"只是第一步,真正难的是选型——不同场景(表格、feed、聊天、分组)该用哪个方案,不同数据量级该不该用、用什么级别的虚拟化,以及各种边界情况下的 fallback。

本文把主流方案和大厂实践完整调研一遍:原理 → 方案全景 → 场景选型 → 量级判断 → fallback。


1. 虚拟列表是什么:先看清问题的量级

不虚拟化的问题有多严重?一个直观对比:

直接渲染虚拟列表
10 万条数据10 万 + 个 DOM 节点约 50~80 个节点
首次渲染可能冻结 / 卡死毫秒级
滚动大量布局 + 绘制只重算视口窗口

为什么 DOM 多就慢:浏览器对每个节点都要维护数据结构、做布局计算、参与绘制。节点多 → 布局(Layout)慢、绘制(Paint)慢、内存占用高。50 万行直接渲染,浏览器基本会冻结甚至崩溃。

虚拟列表的核心思路三件事:

  1. 总高度占位:用一个"撑高"的容器模拟全部内容的高度,让滚动条长度正确;
  2. 只渲染窗口内:根据 scrollTop 算出当前视口应该显示哪些项(startIndex~endIndex),只渲染这些;
  3. 绝对定位 / translateY 偏移:把每一项按它在整条列表中的位置放好,视觉上无缝衔接。

overscan(缓冲区):视口外再多渲染几个,防止快速滚动时出现白屏——一般 5~10 个为宜。


2. 核心分水岭:定高 vs 不定高

这是选型的第一道分岔,也决定实现的复杂度和用哪个库。

维度定高(Fixed Height)不定高(Dynamic Height)
每项高度固定,index * height 直接算位置不一致,需要测量 / 预估
定位O(1)index * itemHeight需位置缓存:测量可见项 + 预估其余
实现成本低,几乎不踩坑高:高度缓存 + 预估修正 + ResizeObserver
滚动稳定性最稳测量偏差会导致滚动条跳动,需补偿
典型场景后台表格、结构规整的列表评论、feed 流、混合复杂布局

决策逻辑一句话:内容可控(表格、纯文本行)→ 定高;内容不可控(feed、评论、图文混排)→ 不定高。定高永远是最优解,不定高是业务逼出来的复杂实现。

不定高是怎么做的:只测量渲染过的,其余靠预估

所有主流实现用的都是同一套策略(react-window 的 VariableSizeList、TanStack 的 measureElement、react-virtuoso 内置的都是):

  1. 给每项一个预估高度 estimateSize(通常 100px 起步,其实不精确也没关系);
  2. 只在视口内实际渲染的项,用 ResizeObserver / getBoundingClientRect 测量真实高度,存进缓存;
  3. 位置由"已测量的精确值 + 未测量的预估值"累加得到——随着滚动,越来越多的项被测量,预估会被真实值逐步替换,位置越来越准;
  4. 当某项尺寸变化(图片加载、行展开)导致视口上方的项高度改变时,做滚动补偿scrollTop += 变化量,让用户看到的视觉位置不跳动。
// 量级化的实现思路:位置 = 已测量项的精确高度 + 未测量项的预估高度
function getItemOffset(index) {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += measuredHeights.get(i) ?? estimateSize(i); // 测过的用真实值,没测的用预估
}
return offset;
}

这就是"只测量渲染过的,其余全靠预估,用多少测多少"的 fallback 思想——它在精度和性能之间取平衡,也是后面"各种量级 fallback"的雏形。


3. 主流方案全景(大厂 / 开源)

方案定位体积定高/不定高擅长
react-window轻量基础组件~4-7KB定高为主(不定高要手写测量)定高列表/表格,性能极致
react-virtuoso开箱即用全能方案~30KB+动态高度内置(ResizeObserver 自动测量)聊天/feed/动态内容
@tanstack/react-virtualheadless 底层 hook~15-18KB定高/不定高都支持,测量可控企业表格、跨框架、自定义渲染
rc-virtual-listAnt Design 内部组件~148KB定高为主antd 系表格/列表
ahooks useVirtualListReact Hook轻量支持动态高度蚂蚁生态快速接入
vue-virtual-scroller / Element PlusVue 生态支持动态高度Vue 项目

3.1 react-window:定高场景的性能标杆

  • 优点:包体积最小、纯组件性能最好、避免布局抖动(layout thrashing),是 2019-2022 年的行业标准,存量项目极多;
  • 缺点动态高度需要自己维护测量逻辑VariableSizeList 只给机制不给自动测量);不支持分组、sticky header、反向滚动、嵌套滚动;官方不建议新项目再用,基本停止新功能开发;
  • 适用:数据看板、定高表格这类"高度可预测 + 极致性能"的场景。

3.2 react-virtuoso:动态场景的开箱即用之王

  • 优点:动态高度全自动(ResizeObserver 测量 + 高度缓存,零手动维护);内置 TableVirtuoso(表格)、VirtuosoGrid(网格/瀑布流)、GroupedVirtuoso(分组 + sticky 组头);反向滚动 + followOutput(聊天消息流跟随最新消息);TypeScript 完整;
  • 生产案例Rocket.Chat 的消息列表就是用它——同时用"双向加载 + 动态高度 + 日期分隔符",是业界最严苛的虚拟化场景;
  • 适用:聊天、feed、评论这类"动态高度 + 内容不可控"的场景,中小团队起步首选。

3.3 @tanstack/react-virtual:headless 的底层基础设施

  • 优点框架无关(React/Vue/Solid 都行)、不绑定 DOM(可接 Canvas/WebGL)、TypeScript-first;底层用 Float64Array 连续数组做位置缓存(O(1) 访问、少 GC),能扛几十万条;与 @tanstack/react-table 组合是企业级数据表格的推荐模式;
  • 缺点:headless,要自己处理 ref、resize、scroll 同步,不是开箱即用;
  • 适用:企业数据表格、内部工具平台、需要自定义渲染管线的复杂布局。

3.4 蚂蚁系:rc-virtual-list + ahooks useVirtualList

  • rc-virtual-list:Ant Design(antd Table、Select 的虚拟滚动)底层的虚拟列表组件,周下载百万级;
  • ahooks useVirtualList:一行 hook 接入,支持动态高度,适合蚂蚁生态 / 快速原型。

4. 各场景的大厂选型

场景 1:后台数据表格(大量定高行)

固定列 + 海量行、每行高度基本一致 → 定高虚拟化。React 侧用 react-window@tanstack/react-virtual + @tanstack/react-table;antd 系直接用 rc-virtual-list 或 Table 的虚拟滚动。

真实案例(企业后台 10 万行 × 20 列表格):初始渲染 12s → 0.5s,CPU 80% → 15%

场景 2:feed / 评论流(动态高度)

图文混排、高度不可控 → 动态高度虚拟化,选 react-virtuoso(自动测量)或 @tanstack/react-virtual(自己接测量)。

真实案例(电商商品列表):首屏 3.2s → 0.8s,内存 400MB → 100MB,帧率 20 → 60 FPS

场景 3:聊天消息流(反向 + 动态 + 跟随)

这是最苛刻的场景:消息从底部往上排(反向滚动)、每条高度不同、还要跟随最新消息。react-virtuoso 是事实标准(Rocket.Chat 生产案例),followOutput 平滑滚动到最新。

场景 4:分组列表 / 树形(sticky 组头)

分组 + 吸顶组头 → react-virtuosoGroupedVirtuoso;树形展开/收起会改高度,属于"动态高度 + 增量测量"的复合场景。

场景 5:网格 / 瀑布流

VirtuosoGrid(多列)+ 每格动态高度。

场景 6:虚拟化 + 分页 / 无限滚动组合

服务端分页管"数据边界",客户端虚拟列表管"渲染边界"。注意一个坑:接口返回 total_count 和实际数据可能不一致,会导致滚动条过长/过短——用"已知高度 + 估算未加载部分"来对齐滚动条。

场景 7:Hybrid H5(移动端)

真实案例(Hybrid H5 饰品列表 1000+ 行):迁移前 DOM 节点 >20000、滚动 30fps 以下;虚拟化后显著改善。移动端选型优先 react-window(轻量)或动态场景的 react-virtuoso


5. 各种量级的 fallback:什么时候该虚拟化,用什么级别

"上不上虚拟列表"和"上哪种虚拟化",本质取决于数据量级。这一步做对,能省掉大量无谓的复杂度。

5.1 量级判断表

数据量级策略说明
< 1000不虚拟化,直接全渲染虚拟列表本身有实现复杂度 + 测量开销,这个量级收益趋近于零,别为了"显得专业"而上
1000 ~ 5000优先分页 / 无限滚动;或简单定高虚拟开始有卡顿风险(经验阈值),但还没到非虚拟不可
5000 ~ 10 万正式虚拟列表定高优先;动态高度必须上"测量 + 预估 + 补偿"
10 万 ~ 100 万高性能虚拟化二分定位(O(log n))、位置缓存、增量重算,避免 O(n) 扫描;注意浏览器像素高度上限
100 万 +特殊架构单容器几乎到极限,考虑数据聚合/分批、服务端、Canvas/WebGL 渲染

5.2 量级对应的实现强度

5k~10 万(普通虚拟化):线性累加位置可接受,overscan 5~10,定高走 O(1) 公式。

10 万~100 万(高性能虚拟化):这一步要跨过三道坎——

  1. 定位别用线性扫描:从 scrollTop 找起始项,用二分查找 O(log n),否则每次滚动都 O(n);
  2. 位置数据别用对象数组:TanStack 用连续 Float64Array 存 start/size,避免几十万个对象的内存与 GC 压力;
  3. 测量别全量做:只测视口内的项,其余用预估;增量地重算受影响位置,别每次滚动全量重算。

浏览器硬边界:DOM 元素/滚动容器高度有物理上限(Chrome 约 2^24 ≈ 1600 万 px,实用上滚动到百万像素级已是极限)。这意味着"百万条 × 每条 20px = 2000 万 px"这种组合,单靠一个滚动容器是放不下的——必须拆(分批加载、按需扩展高度)或换渲染载体。

5.3 fallback 的几种形态

"fallback"在这类系统里至少指三件事:

① 数据量级的降级(上面那张表):数据小到不值得虚拟化 → 直接全渲染。虚拟化不是免费的,它有测量开销、高度缓存维护、定位计算——数据只有几百条时,直接渲染更快更简单。

② 测量机制的降级

异常 / 边界fallback
某项还没被渲染 / 没测到estimateSize 预估值(测量后自动替换)
图片异步加载导致高度变化ResizeObserver 监听 + 增量重算受影响区间
视口上方高度变化导致跳动滚动补偿scrollTop += 高度差值 保持视觉稳定
不支持 ResizeObserver(老环境)降级为滚动事件 + getBoundingClientRect 采样
快速滚动出现白屏加大 overscan 缓冲区(5~10 个)

③ 需求不满足的降级:定高方案在遇到动态内容时,如果不想引入大库,可以退化为"预估高度 + 出现内容后再补测修正"(react-window VariableSizeList 就是让你自己接这套);极端情况(无法测量、必须逐条真实高度)甚至放弃虚拟化、退化为分批渲染 + 懒加载


6. 工程实践:性能归因与常见坑

6.1 先做性能归因,别盲目上虚拟列表

虚拟列表只解决"渲染性能(DOM 规模)"问题,不解决数据获取、状态管理问题。先用 Chrome Performance 归因:

  • DOM 数量过多(经验阈值:<1000 通常没问题;1000~5000 有卡顿风险;>5000 基本要虚拟化)→ 虚拟列表是核心解法;
  • Layout 时间长(不定高元素、图片、复杂 CSS)→ 重点是控制高度、降低布局复杂度,虚拟列表帮不上;
  • Scripting 时间长(频繁 render/diff)→ 应做 memo、分层渲染、状态拆分。

6.2 五个高频坑

  1. key={index}:滚动后状态漂移(第 100 行展开,滚动后变第 98 行)——用 item 的业务 id 做 key
  2. 状态存在 item 组件内部:虚拟列表的项会反复卸载/重建,内部 state 会丢——状态提到父级或全局 store,用 Map<itemId, state> 缓存;
  3. overscan 太小:快速滚动出现白屏——缓冲区放到 5~10 个
  4. 滚动回调里做复杂计算:应使用 requestAnimationFrame 或防抖,别阻塞主线程;
  5. 服务端分页 + 虚拟列表混用total_count 与实际数据不一致会导致滚动条失真——用"已知部分精确高度 + 未加载部分估算高度"对齐。

6.3 一个性能对照

场景优化前优化后
10 万行 × 20 列表格初始渲染 12s,CPU 80%0.5s,CPU 15%
电商商品列表首屏 3.2s,内存 400MB,20 FPS0.8s,内存 100MB,60 FPS
Hybrid H5 饰品列表DOM >20000,滚动 <30fps显著改善

7. 一句话总结

虚拟列表的本质是"总高度占位 + 只渲染视口窗口 + 定位偏移",它只解决 DOM 规模问题,不背数据获取和状态的锅。选型的第一分水岭是定高 vs 不定高:内容可控走定高(react-window / TanStack + Table,O(1) 定位、最稳),内容不可控走动态高度(react-virtuoso 自动测量,或 TanStack 自接测量),聊天/feed/分组等特殊场景用对应内置能力。而上不上虚拟、上到什么强度,看数据量级:千条以内直接渲染、万级上正式虚拟、十万到百万上二分 + 位置缓存 + 增量重算、百万以上得换架构。各种 fallback(预估高度、滚动补偿、overscan 加大、ResizeObserver 降级)本质都是"在精度、性能、稳定性之间取平衡"——先归因性能,再决定手段,永远别为了"用上虚拟列表"而虚拟化。


参考

快速吃透 MySQL 索引:B+Tree、聚簇索引、最左前缀、覆盖索引、索引失效

· 阅读需 25 分钟

面试问 MySQL,十有八九会落到索引上。但很多人对索引的理解是碎片化的:知道"建索引能提速""最左前缀""不要前模糊 LIKE",却说不清索引为什么快、B+Tree 好在哪里、二级索引为什么要回表、EXPLAIN 里的 Using index / Using index condition 到底差在哪

这篇文章把 MySQL 索引从头到尾串一遍:从数据结构出发,到物理存储,再到联合索引与优化器行为。看完能自己判断"这个 SQL 该建什么索引、会不会命中"。


1. 索引是什么:从"全表扫描"说起

在没有索引的 MySQL 表上查一行数据,InnoDB 只能从第一个数据页开始,把整张表的所有页依次读进内存,逐行比对条件。这就是全表扫描(full scan)——复杂度 O(n),行数一多就慢。

索引做的事情,本质上和书的目录、字典的拼音检字表一样:额外维护一份"有序的、缩小范围的查找路径",用少量 IO 定位到目标,而不是翻遍全书

索引能提速的本质是把"线性查找"变成"树形查找":读多少次磁盘,取决于树的高度,而不是表的行数。所以下文的一切,都围绕"把树做矮、把路径做窄"展开。