列表一滚就卡:先控制页面里的DOM数量
一个后台页面要展示几万条记录,接口只用了几百毫秒,页面却在渲染时明显停顿。继续滚动,还会出现掉帧。此时如果只盯着接口速度,往往找不到真正的问题。
前端需要为每一行创建元素、计算样式、完成布局,并处理后续更新。即使每行只有几个标签,几万行累积起来也会形成很大的页面结构。先在性能工具里区分网络等待、JavaScript 执行和布局绘制的时间,比直接换框架更有帮助。
如果用户主要逐页查看记录,分页通常是最直接的方案。产品确实需要连续滚动时,可以考虑虚拟列表:保留完整的滚动空间,只创建当前可见范围附近的行。视口外的内容暂时不进入页面,需要时再替换进来。
下面是一个固定行高的简化示例,可以保存为 HTML 文件观察效果。它创建一万条数据,但每次只渲染视口附近的一小段。
<div id="viewport" style="height:320px;overflow:auto">
<div id="spacer" style="position:relative">
<div id="rows" style="position:absolute;inset:0 0 auto"></div>
</div>
</div>
<script>
const viewport = document.querySelector('#viewport');
const spacer = document.querySelector('#spacer');
const rows = document.querySelector('#rows');
const data = Array.from({ length: 10000 }, (_, i) => `记录 ${i + 1}`);
const rowHeight = 36;
const overscan = 5;
spacer.style.height = `${data.length * rowHeight}px`;
function render() {
const start = Math.max(0,
Math.floor(viewport.scrollTop / rowHeight) - overscan);
const end = Math.min(data.length,
Math.ceil((viewport.scrollTop + viewport.clientHeight) / rowHeight)
+ overscan);
rows.style.transform = `translateY(${start * rowHeight}px)`;
const nodes = data.slice(start, end).map(text => {
const row = document.createElement('div');
row.style.cssText = `height:${rowHeight}px;box-sizing:border-box;`
+ 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
row.textContent = text;
return row;
});
rows.replaceChildren(...nodes);
}
let scheduled = false;
viewport.addEventListener('scroll', () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
render();
});
}, { passive: true });
render();
</script>
外层容器负责滚动,中间的占位层提供全部数据对应的高度。内层只承载可见行,并通过位移放到正确位置。额外渲染的五行属于预留区域,可以降低快速滚动时边缘露白的概率。这里用一帧一次的调度合并滚动事件,避免同一帧重复重建内容。
这个示例的前提很明确:每行实际占用的高度始终是三十六像素。边框、内边距、文本换行和图片加载都可能破坏这个前提,因此示例设置了盒模型并限制换行。动态行高需要测量高度、维护累计位置,并在测量变化后调整滚动偏移,复杂度会明显增加。正式项目如果已有成熟组件,优先使用它提供的虚拟滚动能力。
虚拟列表减少的是页面元素数量,数据处理成本仍然存在。如果每次滚动都对全部记录排序、搜索或者执行复杂计算,页面依然会卡。排序和筛选应放在数据变化时处理;可见区渲染则尽量只做当前区间需要的工作。示例每次替换行是为了展示原理,实际组件通常还会复用节点并跳过未变化的区间。
还需要关注交互状态。行离开视口后可能被卸载,输入值、展开状态和选中状态应由稳定的数据标识关联,不能只依赖某个临时节点。键盘焦点、屏幕阅读器、浏览器页内查找也可能受到影响,应根据产品需求选择合适的组件和替代操作方式。
验证优化时,不必先追求一个漂亮的跑分。可以检查滚动到列表中段后,页面中的行节点是否仍然保持在一个较小范围;再用性能录制比较滚动期间的长任务和布局耗时。最后测试快速拖动滚动条、窗口缩放和跳转到末尾,确认没有空白或错位。
数据量大并不意味着必须一次渲染全部数据。先确定用户真正需要看见多少内容,再让页面承担相应的工作量,往往比在每个单元格上零散地加缓存更有效。
- 点赞
- 收藏
- 关注作者
评论(0)