本文详解如何利用虚拟列表和虚拟Table技术解决前端大数据量滚动时的页面卡顿问题。基于React与TypeScript,通过仅渲染可视区域内的DOM元素,大幅减少渲染开销。文章提供了定高虚拟列表及基于Ant Design的虚拟Table的具体实现思路与代码示例,帮助开发者优化长列表性能。
前言
此前在处理不分页直接获取全部数据的前端滚动场景时,页面出现了明显的卡顿现象。虽然电脑浏览器的性能差异有一定影响,但根本原因在于一次性渲染过多数据。为了解决这一问题,通常采用虚拟滚动技术,即只渲染当前可见区域的内容,从而降低浏览器单次渲染的数据量。本文将介绍基于React和TypeScript技术栈的虚拟列表与虚拟Table的实现方法。
虚拟列表
虚拟列表的核心机制是仅渲染当前可视区域内的列表项。它通过监听浏览器的滚动事件,根据容器尺寸和用户滚动位置,动态计算并渲染应当显示的列表项。即便数据总量巨大,系统也仅渲染当前可见部分,这极大地减少了DOM节点数量,从而显著提升页面性能与响应速度。结合下图进行理解。

实现虚拟列表主要包含以下步骤:
- 计算可见区域:依据容器尺寸(例如500px)与单项高度(例如50px),计算出当前可见的列表项数量。在此示例中,可视数据量为10个。
- 监听滚动事件:在容器上绑定滚动事件监听器,以实时获取滚动位置。为使容器具备滚动能力,需在内部添加具有特定高度的空元素以撑开父容器。通过获取scrollTop值,可计算出当前显示的第一项下标(公式为scrollTop除以itemHeight),并据此动态更新展示的数据。
基于上述逻辑,封装一个通用的滚动列表组件。
import _ from "lodash";
import React, { useEffect, useState } from "react";
import { listData } from "./data";
type ListType = {
itemHeight?: number; // 每一项的高度
visibleHeight?: number; // 可见高度
total?: number; // 数据总数
dataSource?: any[]; // 全部数据
};
// 为了看效果我模拟的数据
const myList = Array.from(Array(1000), (item, index) => ({name: `名字${item}`, id: index}));
const List = (props: ListType) => {
const {
itemHeight = 54,
visibleHeight = 540,
total = 130,
dataSource = myList,
} = props;
const [showData, setShowData] = useState<any>([]);
const [offset, setOffset] = useState<any>({ top: 0, bottom: 0 });
const visibleCount = Math.ceil(visibleHeight / itemHeight);
useEffect(() => {
const list = _.slice(dataSource, 0, visibleCount);
const bottom = (total - visibleCount) * itemHeight;
setOffset({ top: 0, bottom });
setShowData(list);
}, [dataSource]);
const onScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
const startIdx = Math.floor(target.scrollTop / itemHeight);
const endIdx = startIdx + visibleCount;
setShowData(dataSource.slice(startIdx, endIdx));
const top = startIdx * itemHeight;
const bottom = (total - endIdx) * itemHeight;
setOffset({ top, bottom });
};
return (
<div
className="virtual"
style={{
height: visibleHeight,
width: "100%",
overflow: "auto",
border: "1px solid #d9d9d9",
}}
onScroll={onScroll} // 在父元素上添加滚动事件监听
>
{/* 可视数据 为了滚动数据一直在可视区。加上顶部偏移 */}
<div style={{ height: visibleHeight, marginTop: offset.top }}>
{_.map(showData, (item, index: any) => {
return (
<div
style={{
display: "flex",
alignItems: "center",
height: itemHeight,
borderBottom: "1px solid #d9d9d9",
}}
key={index}
>
{item.name}
</div>
);
})}
</div>
{/* 底部占位 */}
<div style={{ height: offset.bottom }} />
</div>
);
};
export default List;虚拟Table
虚拟表格与虚拟列表原理相似,是虚拟列表在处理大型表格数据时的特殊应用形式。如同虚拟列表,虚拟表格仅渲染可视区域内的表格单元格,以此优化性能并降低内存占用。在Ant Design 4+版本中,官方已提供基于'react-window'的虚拟列表实现方案,读者亦可参考。本文则基于Ant Design提供的API,通过重写components属性来自定义渲染逻辑:获取可视区的起始与结束下标,仅展示该范围内的数据。具体思路与列表实现基本一致,直接参考代码。
import React, { useEffect, useRef, useState } from "react";
import { Table } from "antd";
import _ from "lodash";
type TableType = {
itemHeight?: number; // 每一项的高度
visibleHeight?: number; // 可见高度
total?: number; // 数据总数
dataSource?: any[]; // 全部数据
};
// 为了看效果我模拟的数据
const myList = Array.from(Array(1000), (item, index) => ({name: `名字${item}`, id: index}));
const VirtualTable = (props: TableType) => {
const {
itemHeight = 54,
visibleHeight = 540,
total = 130,
dataSource = myList,
} = props;
const [point, setPoint] = useState<any>([0, 20]);
const [offset, setOffset] = useState<any>({top:0, bottom: 0 });
const tabRef = useRef<any>();
const containRef = useRef<any>();
const visibleCount = Math.ceil(visibleHeight / itemHeight);
useEffect(() => {
const bottom = (total - visibleCount) * itemHeight;
setOffset({ bottom });
setPoint([0, visibleCount]);
const scrollDom = tabRef?.current?.querySelector(".ant-table-body");
console.log("aaa",scrollDom);
if (scrollDom) {
containRef.current = scrollDom;
containRef.current.addEventListener("scroll", onScroll);
return () => {
containRef.current.removeEventListener("scroll", onScroll);
};
}
}, [myList]);
const onScroll = (e: any) => {
const startIdx = Math.floor(e?.target?.scrollTop / itemHeight);
const endIdx = startIdx + visibleCount;
const bottom = (total - endIdx) * itemHeight;
const top = startIdx * itemHeight;
setOffset({top,bottom});
setPoint([startIdx, endIdx]);
};
const columns = [
{ title: "ID", dataIndex: "id", width: 150 },
{ title: "名字", dataIndex: "name", width: 150 },
];
return (
<Table
ref={tabRef}
className="virtual-table"
pagination={false}
columns={columns}
dataSource={dataSource}
scroll={{ y: visibleHeight }}
components={{
body: {
wrapper: ({ className, children }: any) => {
return (
<tbody className={className}>
{children?.[0]}
<tr style={{height: offset.top}}/>
{_.slice(children?.[1], point?.[0], point?.[1])}
<tr style={{height: offset.bottom}}></tr>
</tbody>
);
},
},
}}
/>
);
};
export default VirtualTable;在上述代码中,使用了Ant Design的Table组件内的components.body.wrapper定制表格内容区域的包装器组件。该组件用于包装表格内容,并允许自定义显示逻辑。components.body.wrapper函数接收一个对象参数,包含以下属性:
className:传递给tbody标签的类名字符串,可用于自定义样式。children:表格内容区域的子元素,即表格的数据行与列。
在提供的代码中,components.body.wrapper函数接收一个包含className和children属性的参数对象。在函数内部,children被分割为三部分处理:
children?.[0]:表格的标题行,即表头部分,对应<thead>标签。{_.slice(children?.[1], point?.[0], point?.[1])}:表格的数据行,根据point的取值进行切片,仅渲染point范围内的数据行,对应<tr>标签。<tr style={{height: offset.bottom}}></tr>:底部占位行,用于确保滚动时表格底部内容正确显示,对应<tr>标签,并通过style设置高度为offset.bottom。
其中,point和offset由其他逻辑计算得出,可能在组件的其他部分定义或使用。
通过自定义components.body.wrapper函数,可实现对表格内容更灵活的渲染与定制。此处主要用于实现虚拟表格功能,仅渲染可见区域的数据行,从而优化大型表格的性能表现。
总结
本文仅实现了固定每项列表高度的虚拟列表。在实际场景中,不定高列表更为常见,其实现复杂度更高,原理相同但需额外计算渲染后的实际高度。本文内容源于项目实践,旨在记录以备后续查阅。
以上就是详解JS如何解决大数据下滚动页面卡顿问题的详细内容,更多关于JS解决页面卡顿的资料请关注本站其它相关文章!