面试官非常喜欢问这道极限压测题:“后端犯浑,一个接口一口气直接返回了 10 万条列表数据给你。页面不能分页,要求你全部渲染在这个页面上且不能把页面卡死白屏,你该怎么做?”
这背后考察的,其实是前端应对大数据量导致的主渲染线程阻塞的解题能力与大局观。
哪怕是极简结构,向页面的 DOM 树内硬生生地塞入 100,000 个独立的 <div> Node节点。
针对这道题,我们从基础到极致,主要有三套解法。
既然一次性塞入十万条会堵塞血管,那我们能不能“蚂蚁搬家”,一次只塞几十条,把长卡顿切碎成用户无法感知的超短小执行块呢?
这就是时间分片的核心。我们利用浏览器的原生高级 API requestAnimationFrame(每一帧渲染之前执行的回调),把十万条数据切分成无数小批次,趁浏览器在每一帧刷新页面的空隙,塞进几百条数据。
评价:用 requestAnimationFrame 切了片,首屏瞬间就出来了,页面也不会假死。但它有一个致命缺陷:当几秒之后这十万个节点最终还是全部塞进了页面里,你的 HTML 依然挂载着多达数十万个真实 DOM!此时只要页面结构稍有风吹草动引发回流,拖拽滚动条,依然能感受到如同陷入了深深沼泽泥潭般的惊雷卡顿。
要彻底解决这道题,并满足生产环境企业级苛刻的流畅度要求,唯一真神是——虚拟长列表技术。
这是包括 VS Code 的代码行、巨无霸表格组件(AntD Table的虚拟模式)、大长文的动态评论区都在使用的终极利器。
核心原理:视口切除(只渲染你此刻能看见的范围)。
既然用户的屏幕高度可能只有 800px,一屏顶破天只能看到几十行数据。那我何苦在背后渲染十万个真实节点?
100000 * 每行高度 50px = 5百万px 的极高不可见空 <div>。这样一来,浏览器的滚动条直接变得极其短小,用户觉得里面仿佛沉淀了万条长河。scroll 滚动行为并获取目前的 scrollTop。startIndex = Math.floor(scrollTop / 每行高度),我就瞬间算出:哦,此时用户的窗口正好滑到了第 4500 项的位置。[4500] 到 [4530] 这短短 30 个真实卡片 DOM。并随着用户上下拖拽滚轮,不断的销毁上方滚出的节点,补充新进入视口的下方节点。不管后台吐给我十万条还是一个亿的数据,除了作为 JS Array 原生变量占点内存外,真实写入到页面进行重绘计算回流的 DOM 树只有这区区 30 棵树杈子。自然快如闪电。
由于实现中常常要应对“高度不等的列表项动态测量缓存”、“滚动性能节流”、“缓冲挂载区”等深坑,绝大多数工程里不会选择手磨这套系统。由于我们在前文无限滚动方案篇中曾有所涉猎,企业内可以直接祭出杀器:
react-window, react-virtualizedvue-virtual-scrollerQ:遇到海量数据甚至超十万的数据要在单页纯前台展示不能翻页,你会如何处理解决因节点过多造成的白屏或灾难卡顿?
回答思路:
appendChild 会阻塞长达几秒的 UI Main Thread 让页面白屏卡死挂掉,而这归根到底是大量重算排版带来的性能恶化和巨量对象的内存积压。DocumentFragment 内存片段包裹,通过递归包裹 requestAnimationFrame(或新版的 requestIdleCallback 更好),在不阻塞系统绘制的高帧切片零碎时间里“分批塞进去”。强调这种方法能保命首屏呈现,但最后真实 DOM 太深还是会带来滚动操作时的掉帧。scrollTop 计算边界绝对定位,实时偷天换日更换挂载容器内展示的那几十个节点值”。指出这是真正一劳永逸拔掉卡死隐患的工程级答案。