一次性向页面注入 50 万个 <li> 是一个典型的渲染性能瓶颈问题。直接操作 DOM 插入这么多元素会导致浏览器卡死、占用内存过高,甚至触发长时间脚本警告。
要从根本上提升性能,主要思路有两个:减少直接 DOM 操作次数和只渲染用户能看到的部分。
以下是几种主流且有效的优化方案:
方案一:文档片段 + 一次性插入
这是最简单直接的优化,核心是避免在循环中频繁操作 DOM,而是将元素在内存中组装好,一次性追加到页面。
javascript
// 获取容器constcontainer=document.getElementById(‘list-container’);// 1. 创建文档片段constfragment=document.createDocumentFragment();// 在内存中循环创建 50 万个 lifor(leti=0; i<500000; i++){constli=document.createElement(‘li’);li.textContent=`Item ${i}`;// 2. 将 li 先放入片段fragment.appendChild(li);}// 3. 一次性将片段中的 50 万个节点追加到 DOM 树container.appendChild(fragment);
优点:只需一次重排和重绘,比逐个插入快几十倍。
缺点:虽然插入动作快了,但渲染 50 万个节点本身对 CPU 和内存的压力依然存在,滚动会非常卡顿。
方案二:分批延时渲染
既然一次性渲染会阻塞主线程,可以利用 setTimeout、requestAnimationFrame 或 MessageChannel 将任务拆分成多个小批次,让浏览器在间隙有时间响应用户操作。
javascript
constcontainer=document.getElementById(‘list-container’);consttotal=500000;constbatchSize=200;// 每批渲染 200 个letindex=0;functionrenderBatch(){if(index>=total)return;// 使用文档片段组装当前批次constfragment=document.createDocumentFragment();for(leti=0; i<batchSize&&index<total; i++, index++){constli=document.createElement(‘li’);li.textContent=`Item ${index}`;fragment.appendChild(li);}container.appendChild(fragment);// 让出主线程,继续下一批渲染setTimeout(renderBatch,0);}renderBatch();
优点:页面不会卡死,能保持响应。
缺点:随着列表变长,DOM 节点依然巨大,内存占用高,滚动性能依然堪忧。
方案三:虚拟滚动 —— 推荐方案
虚拟滚动是目前处理长列表的最优解。原理很简单:只渲染用户当前视野内能看到的部分。
总数据量:50 万条数据。
可视区域:例如视口高度只能容纳 20 个 <li>。
实际渲染:只渲染这 20 个(加上上下缓冲几行,避免快速滚动时白屏)。
实现思路:
需要一个固定高度的容器,设置 overflow-y: auto。
用一个占位元素(padding 或一个高度为 总条数 * 行高 的 div)撑起滚动条。
监听容器的 scroll 事件。
根据滚动距离 scrollTop 计算当前应该显示哪几条数据。
动态更新可视区域内的列表项内容,并调整它们的 transform: translateY() 位置。
可以使用成熟的库:
React:react-window 或 react-virtualized
Vue:vue-virtual-scroller 或 vue-virtual-scroll-list
原生 JS:可以自己实现,也可以使用 Clusterize.js 等库。
方案四:结合 CSS 属性优化
如果由于某些原因不能使用虚拟滚动,且必须一次性渲染,可以考虑开启浏览器的硬件加速来缓解重绘压力:
css
#list-container{/* 开启独立的渲染层,滚动时不会影响页面其他部分的重绘 */ will-change: transform;/* 或者使用 transform 创建一个新的层叠上下文 *//* transform: translateZ(0); */}
总结建议
如果是实际项目:不要犹豫,直接采用虚拟滚动方案。这是唯一能保证 50 万条数据流畅交互(滚动、点击)的方法。
如果是一次性展示静态数据:可以采用文档片段 + 分批延时渲染,虽然滚动起来会卡,但至少页面能加载出来,不会直接崩溃。
如果数据需要动态更新:请务必选择虚拟滚动,否则数据变化后 DOM 的重新渲染代价太高。
终极建议:50 万条数据全部展示在用户面前,通常不太符合交互设计。是否可以考虑增加搜索、分页或分组筛选,让用户不必一次性看到所有数据?
方案三:虚拟滚动 —— 推荐方案
虚拟滚动是目前处理长列表的最优解。原理很简单:只渲染用户当前视野内能看到的部分。
总数据量:50 万条数据。
可视区域:例如视口高度只能容纳 20 个 <li>。
实际渲染:只渲染这 20 个(加上上下缓冲几行,避免快速滚动时白屏)。
实现思路:
需要一个固定高度的容器,设置 overflow-y: auto。
用一个占位元素(padding 或一个高度为 总条数 * 行高 的 div)撑起滚动条。
监听容器的 scroll 事件。
根据滚动距离 scrollTop 计算当前应该显示哪几条数据。
动态更新可视区域内的列表项内容,并调整它们的 transform: translateY() 位置。
可以使用成熟的库:
React:react-window 或 react-virtualized
Vue:vue-virtual-scroller 或 vue-virtual-scroll-list
原生 JS:可以自己实现,也可以使用 Cl usterize.js 等库 ,不用第三方库怎么优化
如果不使用第三方库,手动实现一个虚拟滚动核心也并不复杂。其本质是监听滚动事件 -> 计算可视区起始索引 -> 更新可视区内容 -> 利用绝对定位或transform将列表项推送到正确位置。
下面我为你提供一个原生 JavaScript 实现的完整虚拟滚动示例,包含核心逻辑和必要的边界处理。
基础 HTML 结构
html
<!DOCTYPEhtml><html><head><style> .viewport { height: 400px; /* 可视区域固定高度 */ overflow-y: auto; /* 出现滚动条 */ position: relative; border: 1px solid #ccc; /* 开启硬件加速 */ will-change: transform; } /* 占位符:用于撑开滚动条,高度为 总条数 * 行高 */ .scroll-phantom { position: absolute; left: 0; top: 0; right: 0; z-index: -1; visibility: hidden; /* 隐藏但占据空间 */ } /* 实际渲染内容的容器,采用绝对定位跟随滚动 */ .content { position: absolute; left: 0; top: 0; right: 0; } .list-item { height: 30px; /* 每个列表项固定高度 */ line-height: 30px; padding: 0 12px; border-bottom: 1px solid #eee; box-sizing: border-box; }</style></head><body><divid=“app”></div><scriptsrc=“virtual-scroll.js”></script></body></html>
核心 JavaScript 实现
关键优化点解析
双容器结构
Phantom(占位容器):高度设置为 总条数 * 行高,用于撑开真正的滚动条。
Content(内容容器):采用 absolute 定位,其内部的列表项通过 transform: translateY() 定位到视觉上的正确位置。
使用 transform 代替 top
transform 由 GPU 加速处理,不会触发浏览器的重排(Layout),性能远优于修改 top 属性。
requestAnimationFrame + 索引变化判断
滚动事件触发非常频繁,使用 requestAnimationFrame 将渲染操作与屏幕刷新率同步,避免不必要的计算。
只有当 startIndex 真正变化时才重新渲染,减少重复工作。
overscan 缓冲机制
额外渲染上下各 overscan 行。这样当用户快速滚动时,新的数据已经提前渲染好了,不会出现白屏闪烁。
文档片段(Fragment)
即使只渲染 20~30 个节点,使用 createDocumentFragment 也比逐个 appendChild 性能更好。
如果要支持动态高度
上面的例子基于固定高度(30px)。如果项目高度不固定,需要更复杂的处理:
方案A:预先估计一个平均高度,然后滚动过程中动态测量并缓存每个项的实际高度,实时调整 phantom 的总高度和每个项的 translateY 偏移量。
方案B:使用第三方库。处理动态高度的逻辑非常复杂,涉及二分查找和缓存算法,这种情况下建议直接使用 react-window 或 vue-virtual-scroller,它们都提供了动态高度的支持。
这个原生实现足够应对 50 万条固定高度数据的流畅渲染,且代码量较小,适合集成到不使用框架的简单项目中。