前端大列表滚动为什么会掉帧:虚拟列表、批量渲染与 60 FPS 验收
不少人维护的商品后台订单列表,数据量从800行涨到12000行之后,最先出问题的往往不是接口响应变慢,而是滚动鼠标滚轮的时候,整个表格仿佛被按住了一样卡顿。打开Performance面板一看,满屏的长任务和频繁布局,真正拖慢交互的原因其实很简单:一口气把所有行全部交给DOM渲染,以及筛选结果变化时连续触发无间隔的渲染操作。
这类问题通常要同时收缩两个优化方向:屏幕可视区域附近只保留少量行,数据变化时按批次把任务逐步交给浏览器执行。虚拟列表用来压缩常驻DOM的总数量,批量渲染用来避免主线程被一次性占满;只改其中任意一处,想稳定守住60 FPS往往很难实现。
- 先用Performance面板录制滚动和筛选的两条基线,不要凭感觉盲目修改组件逻辑。
- 固定行高的列表优先使用可精确计算的可视窗口,保留上下缓冲区域避免快速滚动时露白。
- 大批量更新要拆分成小批次,每批执行完之后主动让出主线程控制权。
- 最终验收要同时看长任务占比、掉帧比例和输入延迟三个维度,不能只参考平均渲染时间。

先把掉帧拆成两个可测问题
滚动掉帧和筛选卡顿经常同时出现,但二者走的不是同一条处理链路。滚动场景下主要看每帧耗时有没有超过约16.7ms,筛选场景下则要看一次状态更新会不会生成超长任务。如果把两种场景混在一起调试,优化完之后很容易得到“滚动流畅了一点,搜索还是很卡”的错误判断。
| 场景 | 重点指标 | 先查什么 |
|---|---|---|
| 连续滚动 | 掉帧、帧耗时、输入延迟 | 可见 DOM 数量与布局次数 |
| 筛选刷新 | 长任务、响应延迟 | 单次更新行数与脚本耗时 |
| 快速跳页 | 空白、抖动、内存 | 缓冲区与节点回收 |
先在同一台机器、同一个数据集下录制10秒连续滚动操作。把“能流畅滚动”这种模糊感受改成可以横向对比的基线:比如可视区域显示30条、总数据12000条、滚动期间长任务出现次数和超过16.7ms的帧占比。这个结果只能对应当前实现的表现,不代表换浏览器后效果也完全一致,所以后续要固定验收环境。
虚拟列表如何把 DOM 数量压到可控范围
固定行高场景下,滚动容器的顶部偏移量可以直接换算出当前需要渲染的起始数据索引:
const rowHeight = 36;
const overscan = 6;
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const visibleCount = Math.ceil(viewportHeight / rowHeight) + overscan * 2;
const end = Math.min(rows.length, start + visibleCount);
const visibleRows = rows.slice(start, end);
const offsetTop = start * rowHeight;
外层容器仍然要保留总高度,否则滚动条会因为只剩几十个节点而失去真实比例。常见做法是用一个占位层撑起 rows.length * rowHeight 的高度,再把当前窗口放到 offsetTop 处。上下各留几行缓冲,能覆盖滚轮事件和触控惯性之间的短暂空档。
这里别急着把行高写死成固定值。如果每行的高度会因为文本换行、图片加载或者展开面板动态变化,索引换算出来的结果会持续失真,最终表现为列表跳动、行重叠或者滚动还没到底就提前触发触底事件。遇到这种情况,应该先限制内容区域的布局规则,或者改成行高测量缓存机制,没有稳定的高度模型支撑,虚拟化只会把表面问题藏得更深,后续调试更麻烦。
筛选和刷新为什么还需要批量渲染
虚拟列表只是减少了最终需要留在页面上的节点总数,筛选操作本身仍有可能在主线程里同步处理12000条全量数据,接着一次性同步创建整批行节点。更稳妥的处理方案是先把筛选结果计算完成,再把显示更新拆分成多个小批次,主动让出浏览器处理输入事件和绘制的机会:
function appendInBatches(items, render, batchSize = 80) {
let cursor = 0;
function flush() {
const next = Math.min(cursor + batchSize, items.length);
render(items.slice(cursor, next));
cursor = next;
if (cursor
批次大小也不是越小越好。同样的80行数据,在办公本上运行刚好在预算内,但如果单元格内部逻辑复杂,执行耗时还是可能超过单帧阈值。实际调试时可以在Performance面板里观察每批脚本的执行耗时,再把批次调整到16.7ms预算以内,同时预留出足够的绘制余量。如果数据过滤逻辑本身计算量很大,可以把纯计算逻辑移到Worker里处理,主线程只负责接收最终结果和维护可视窗口。

一次验收要同时看滚动、筛选和回退
- 固定12000行测试数据和浏览器窗口尺寸,录制连续滚动10秒的表现。
- 重复执行关键词筛选、清空筛选和快速拖动滚动条跳转到末尾的操作,记录过程中的长任务和输入延迟。
- 检查列表顶部、底部、快速拖动滚动条时会不会出现空白、抖动或者重复渲染行的问题。
- 临时关闭虚拟化功能做对照测试,确认优化收益来自节点数量精简和批量渲染策略,而不是测试数据集偶然变小带来的偏差。
不少开发者会把“滚动时超过16.7ms的帧占比明显下降、筛选过程中没有连续长任务、快速滚动全程无空白”作为第一道验收门槛,之后再去低端设备上做兼容测试。如果列表行高不稳定、用户频繁展开详情面板,或者列表总数据量并不大,用普通分页方案实现反而会比虚拟列表更容易维护。性能优化的终点从来不是代码写得更复杂,而是交互时延预算能被稳定复现和保障。
常见问题:虚拟列表上线前还要确认什么
虚拟列表一定比分页快吗?
不一定。需要连续浏览大量内容、频繁快速滚动的大列表场景更适合做虚拟化;用户按条件查询、单页只展示几十条的后台表格,用普通分页实现往往更简单稳定。
为什么虚拟列表滚动时会出现白块?
通常是缓冲行数量太少、渲染批次太大或者行高估算错误导致的。可以先适当增加上下缓冲的预渲染行数,再检查实际行高有没有被异步图片加载和内容换行操作改变。
批量渲染能不能直接用 setTimeout?
可以用来做粗粒度的主线程让出操作,但它的执行时机不会和下一帧绘制同步。如果需要紧贴浏览器的绘制节奏调整渲染逻辑,优先使用requestAnimationFrame,并重新评估每批任务的工作量。
怎么判断该上 Worker?
当筛选、排序或者数据转换本身占用大量CPU时间,而整个计算任务又不依赖DOM操作的时候,再考虑把逻辑放到Worker里运行;所有DOM更新操作仍然必须回到主线程执行。
把优化结果留成可复测的记录
最后要保存好测试时的数据规模、设备型号、浏览器版本、窗口尺寸、虚拟窗口参数、批次大小和三组核心指标。下次组件升级或者单元格内部逻辑变更时,直接用同一套测试流程回放验证,就能明确感知到性能变化是来自数据量改动、样式逻辑调整,还是渲染策略本身出现了退化。
CSS scroll snap 怎么做横向卡片滑动:scroll-snap-stop、触摸手势与键盘验收
- 上一篇
- CSS scroll snap 怎么做横向卡片滑动:scroll-snap-stop、触摸手势与键盘验收
- 下一篇
- Go context.WithCancelCause 怎么传递取消原因:错误链与测试验收
-
- 文章 · 前端 | 3小时前 | 前端 · 性能优化 ·
- 前端性能预算怎么落地:图片、脚本与交互延迟阈值
- 446浏览 收藏
-
- 文章 · 前端 | 5小时前 |
- 表单校验怎样同时服务键盘用户与屏幕阅读器
- 225浏览 收藏
-
- 文章 · 前端 | 7小时前 | 前端 · css ·
- CSS 层叠层解决组件库与业务样式优先级冲突
- 464浏览 收藏
-
- 文章 · 前端 | 10小时前 |
- 浏览器缓存命中却仍请求服务器:强缓存与协商缓存排查
- 477浏览 收藏
-
- 文章 · 前端 | 12小时前 | javascript · 懒加载 IntersectionObserver 前端队列
- 用 IntersectionObserver 构建可停止的懒加载队列
- 401浏览 收藏
-
- 文章 · 前端 | 1天前 |
- 前端状态更新频繁时,批处理与去抖分别解决什么
- 227浏览 收藏
-
- 文章 · 前端 | 1天前 | 前端 · 可访问性 · 焦点陷阱 HTML dialog 焦点恢复 inert 无障碍弹窗
- 可访问弹窗的焦点陷阱、关闭恢复与背景隔离
- 309浏览 收藏
-
- 文章 · 前端 | 1天前 | 前端 · 性能优化 · javascript · ArrayBuffer postMessage 前端性能 Web Worker Transferable structured clone
- Web Worker 传大数据为何卡顿:复制与 Transferable 对比
- 220浏览 收藏
-
- 文章 · 前端 | 1天前 | 请求超时 Fetch AbortController AbortSignal 用户取消 前端异常处理
- Fetch 请求取消后,超时与用户中断要怎样区分
- 466浏览 收藏
-
- 文章 · 前端 | 1天前 | 文件上传 · javascript · 前端开发 · 大文件上传 断点续传 XMLHttpRequest Blob.slice 前端分片上传 暂停上传
- 前端上传大文件:分片、暂停与失败续传怎样协作
- 371浏览 收藏
-
- 文章 · 前端 | 1天前 | 列表详情 View Transition API 前端渐进增强
- View Transition API 做列表到详情过渡的渐进增强
- 385浏览 收藏
-
- 文章 · 前端 | 1天前 |
- 用 CSS Container Query 让卡片按容器而不是视口响应
- 289浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 381次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 453次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 465次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 405次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 233次使用
-
- Chrome DevTools 如何验证页面能否命中 bfcache:Run Test 与失败原因定位
- 2026-09-04 259浏览
-
- GitHub 如何重建超大型拉取请求的差异渲染
- 2026-10-05 446浏览
-
- 前端虚拟列表实战:10 万条数据如何只渲染可视区域
- 2026-06-13 352浏览
-
- 前端长列表虚拟滚动实战:从可视区计算到滚动流畅
- 2026-06-17 111浏览
-
- 前端长列表卡顿怎么优化:从全量 DOM 到虚拟列表架构
- 2026-07-01 260浏览

