页面注入50万个li怎么做提升性能?

一次性向页面注入 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 万条固定高度数据的流畅渲染,且代码量较小,适合集成到不使用框架的简单项目中。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容