
一、一张名册把浏览器拖住了去年秋天给一个镇做常住人口名册需求看着很平常几万行数据、三十多列字段还要支持按村组、年龄段、参保状态等七个条件组合筛选。第一版做得很老实前端把筛选条件原样发给后端后端一条语句查全量接口把结果整体返回。测试那天用的是两万行左右的样本接口响应体四十一兆首屏从点击菜单到表格可以滚动用掉十一秒多中途浏览器标签页直接提示内存占用过高看进程峰值冲到了一千三百兆。真正上线之后数据量翻了三倍全镇六万两千行、三十四列Excel 导出单次就有十八兆。表格里的现象变得很直白鼠标滚轮往下滚两屏就开始掉帧输入框敲一个字要等一秒多才有回显因为每一次按键都可能触发一次针对全量数组的过滤计算。后来我们干脆在看板上加了一行提示让同事在数据量大的时候少用组合筛选这行提示本质上是我们把自己的问题转嫁给了使用者。二、慢的不是表格组件是一次搬全量第一反应是换个表格组件我们试了三个开源实现把同样的两万行数据喂进去差别其实不大。原因也很清楚瓶颈不在组件的算法上而在把六万行、三十四列的数据整体搬进浏览器内存这件事本身。单行三十四个字段按平均二十字节算光原始数据就接近四十兆再加上 Vue 的响应式代理要给每个字段挂上依赖收集实际占用会被放大好几倍内存里全是这些互相引用的对象。另一个被忽略的消耗是 DOM。就算行高压到三十二像素六万行铺开也是一百八十多万个节点时刻准备着浏览器要为它们计算布局和样式滚动时每一次重排都在跟这个数字较劲。前端过滤之所以慢是因为条件一变就要把六万个对象全部重新比较一遍而其中九成九的结果根本不会出现在视口里纯属白算。直觉做法是加个骨架屏或者改成分批渲染把首屏的感知时间藏起来。这两招能改善体感但没有解决数据量的问题滚到列表底部照样卡筛选照样慢而且分批渲染会让全选和统计功能的实现变得难以预料。我们需要的不是把慢藏起来而是从一开始就别把不需要的数据搬到前端。三、三条路各自的账第一条路是纯服务端分页每页固定一百行翻页时重新请求。这条路实现最简单后端压力可控前端拿到多少渲染多少内存占用跟数据量彻底脱钩。它的代价也很直白用户看见的是一页一百行跨页统计无从谈起翻到第九十页要等一次网络往返而筛选条件一变就得从第一页重新开始操作节奏被打断得很厉害。第二条路是纯前端虚拟滚动数据仍然全量到前端只渲染视口里的那几十行。滚动的连续感明显更好滑动时像在看本地数据但有一条死结筛选还是得走服务端因为六万个对象在前端逐个匹配响应时间就压不下来。数据全量传输的体积也省不掉首屏那四十兆的下载时间依旧存在网络稍差一点就会退化成一个空白的长条。第三条路是两者叠起来服务端只返回当前窗口需要的数据前端用虚拟滚动把这些数据拼成一根完整的长滚动条。代价是实现复杂度上去了前端要维护一个总行数、当前已加载区间和预取状态任何一处对不上就会出现空白行或者行号跳变。权衡之后我们走的是这条路因为只有它能把首屏时间和滚动流畅度一起压住。四、分页加虚拟滚动怎么落到一起页面大小做成可调默认一百行用户可以在五十、一百、两百三档之间切。虚拟滚动的可视区高度按浏览器窗口算行高固定三十二像素滚动条的总高度等于总行数乘以行高撑出一根和真实数据一样长的滚动条让用户拖起来有正确的距离感。已加载区间之外的行先渲染成等高的占位块滚动到附近时再换成真实单元格这样滚动条不会因为数据到位而忽长忽短。预取提前量定在二十行也就是滚动位置距离已加载区域底部还有二十行的时候就开始请求下一页用这个提前量把网络往返的延迟盖住。请求去重做在客户端同一页码正在飞行中的请求不再重复发出滚动过快时中断上一次未完成的请求。这套分页加虚拟滚动的组合成了万村乐数字乡村 里所有名册页的默认写法。五、几个必须统一的约定筛选条件变化时一律把页码重置到第一页并且清空已加载区间这一条写在名册组件的封装里而不是交给调用方。排序不放在前端做点击表头只是把排序字段和方向带给服务端前端在表头标出当前排序状态同时提示排序作用于全部数据而不是当前这一页。导出走异步任务提交时带上当前的筛选条件和排序任务完成后给一个下载入口不占用名册查询这条链路。全选做成了按条件全选用户点表头复选框时前端提交的是筛选条件加一个标记而不是几万个主键。后端拿到标记后按同样的条件生成操作集返回命中行数让用户确认再做后续处理。取消勾选某一行时才把该行主键放进排除列表正负两个集合都为空时视为全选这样请求体的大小跟数据量无关只跟用户手动处理的条数有关。六、踩过的三个坑第一个坑是全选把请求体撑爆。做批量标记的时候前端把当前筛选命中的两万个主键拼成一个数组发给后端请求体接近三百八十千字节网关的请求体上限是二百五十六千字节浏览器控制台里只看到一行四一三用户以为按钮没反应反复点了五次。根因是全选被实现成了提交主键列表。改法就是前面那套按条件全选请求体缩到几百字节顺带把批量操作的耗时从十四秒压到了不到两秒。第二个坑是排序只对当前页生效造成误导。名册页每页一百行用户点了一下年龄列的表头看到当前这一页按年龄排好了就以为整张表已经排好序接着去核对第一百零一行的数据发现对不上判定我们的数据有问题。根因是排序被实现成了前端对已加载区间做一次排序。改法是把排序交回服务端前端只负责传字段和方向并且在表头加一句排序范围提示把歧义消除在界面上。第三个坑是行高不固定时虚拟滚动算错位置。最早有几个列允许内容换行行高会随内容在两行到四行之间变化滚动条的位置和真实内容对不上滚到一半会突然跳回顶部。根因是虚拟滚动按固定行高换算索引。改法是给名册行强制单行显示超长内容用省略号加悬浮展开把行高锁死在三十二像素需要看完整内容时点开侧边的详情抽屉。七、这层解决不了什么分页加虚拟滚动管的是渲染管不了查询本身。名册的模糊搜索如果写成前后都带通配的匹配六万行的表上依旧要全表扫接口慢的是数据库而不是前端。我们后来给常用的几个筛选字段补了索引把组合条件改写成前缀匹配把这条链路的下限压到了三百毫秒以内但这属于查询优化跟渲染方案没关系别指望前端换一套写法就能把慢查询救回来。跨页统计也是同样的边界。名册页面上写的合计人数、参保人数这类数字必须由后端单独提供不能由前端把已加载的几百行加起来充数否则数据会随着滚动不断变化看上去像在对账实际是在误导。我们把统计做成独立接口跟分页查询并行发出各自维护各自的加载状态界面上的数字来源也标明清楚避免谁把局部结果当成全体。八、小结长列表的问题看着是渲染落到方案上其实是数据搬多少、什么时候搬。我们的选择是把搬数据的单位从整表换成窗口再用虚拟滚动把窗口拼成连续的长条用户感知到的是一张六万行的表浏览器实际持有的只有几百行。这套写法的代价是前端多了一层状态行号、已加载区间、预取标记必须一起维护好处是数据量再涨一倍首屏时间也不会跟着涨。万村乐数字乡村 里行数最多的那张名册有六万多行用这套写法首屏一直稳在一秒上下。回头看最省事的决定是承认表格组件没问题问题在数据交付方式上这句话想明白之前我们花了两周换组件想明白之后改动集中在一个名册封装组件里所有名册页照着它写新增一张名单的工作量从两天降到了半天。