使用proTable编辑渲染卡顿优化方案(可视区域动态渲染)
1. 演示示例
1.1 示例说明
- 渲染一个5000个单元格的列表
- 点击单元格,变色为粉色,全局唯一(选中单元格A,选中B时,取消A的选中)
- 双击单元格,编辑数据,回车保存
1.2 代码演示
- table 列表
import type { ProColumns } from "@ant-design/pro-components";
import { ProTable } from "@ant-design/pro-components";
import { useState } from "react";
import CellBox from "./CellBox";
const valueEnum = {
0: "close",
1: "running",
2: "online",
3: "error"
};
const tableListDataSource: any[] = [];
const creators = ["付小小", "曲丽丽", "林东东", "陈帅帅", "兼某某"];
for (let i = 0; i < 400; i += 1) {
tableListDataSource.push({
key: i,
name: "nameAppName12",
name1: "name1AppName12",
name2: "name2AppName12",
name3: "name3AppName12",
name4: "name4AppName12",
name5: "name5AppName12",
name6: "name6AppName12",
name7: "name7AppName12",
name8: "name8AppName12",
name9: "name9AppName12",
name10: "name10AppName12",
name11: "name11AppName12",
name12: "name12AppName12",
name13: "name13AppName12",
name14: "name14AppName12",
name15: "name15AppName12",
name16: "name16AppName12",
name17: "name17AppName12",
name18: "name18AppName12",
name19: "name19AppName12",
name20: "name20AppName12",
name21: "name21AppName12",
containers: Math.floor(Math.random() * 20),
creator: creators[Math.floor(Math.random() * creators.length)],
status: valueEnum[Math.floor(Math.random() * 10) % 4],
createdAt: Date.now() - Math.floor(Math.random() * 100000),
memo:
i % 2 === 1
? "很长很长很长很长很长很长很长的文字要展示但是要留下尾巴"
: "简短备注文案"
});
}
const columns: ProColumns<TableListItem>[] = [
{
title: "应用名称",
width: 150,
dataIndex: "name"
},
{
title: "应用名称name1",
width: 150,
dataIndex: "name1"
},
{
title: "应用名称name2",
width: 150,
dataIndex: "name2"
},
{
title: "应用名称name3",
width: 150,
dataIndex: "name3"
},
{
title: "应用名称name4",
width: 150,
dataIndex: "name4"
},
{
title: "应用名称name5",
width: 150,
dataIndex: "name5"
},
{
title: "应用名称name6",
width: 150,
dataIndex: "name6"
},
{
title: "应用名称name7",
width: 150,
dataIndex: "name7"
},
{
title: "应用名称name8",
width: 150,
dataIndex: "name8"
},
{
title: "应用名称name9",
width: 150,
dataIndex: "name9"
},
{
title: "应用名称name10",
width: 150,
dataIndex: "name10"
},
{
title: "应用名称name11",
width: 150,
dataIndex: "name11"
},
{
title: "应用名称name12",
width: 150,
dataIndex: "name12"
},
{
title: "应用名称name13",
width: 150,
dataIndex: "name13"
},
{
title: "应用名称name14",
width: 150,
dataIndex: "name14"
},
{
title: "应用名称name15",
width: 150,
dataIndex: "name15"
},
{
title: "应用名称name16",
width: 150,
dataIndex: "name16"
},
{
title: "应用名称name17",
width: 150,
dataIndex: "name17"
},
{
title: "应用名称name18",
width: 150,
dataIndex: "name18"
},
{
title: "应用名称name19",
width: 150,
dataIndex: "name19"
},
{
title: "应用名称name20",
width: 150,
dataIndex: "name20"
},
{
title: "应用名称name21",
width: 150,
dataIndex: "name21"
},
{
title: "容器数量",
width: 90,
dataIndex: "containers",
align: "right",
sorter: (a, b) => a.containers - b.containers
},
{
title: "状态",
width: 80,
dataIndex: "status",
initialValue: "all",
valueEnum: {
all: { text: "全部", status: "Default" },
close: { text: "关闭", status: "Default" },
running: { text: "运行中", status: "Processing" },
online: { text: "已上线", status: "Success" },
error: { text: "异常", status: "Error" }
}
},
{
title: "创建者",
width: 80,
dataIndex: "creator",
valueEnum: {
all: { text: "全部" },
付小小: { text: "付小小" },
曲丽丽: { text: "曲丽丽" },
林东东: { text: "林东东" },
陈帅帅: { text: "陈帅帅" },
兼某某: { text: "兼某某" }
}
}
];
export default () => {
const [active, setActive] = useState("");
const [editKey, setEditKey] = useState("");
const [dataSource, setDataSource] = useState(tableListDataSource);
return (
<ProTable<TableListItem>
dataSource={dataSource}
rowKey="key"
pagination={{
pageSize: 200,
showQuickJumper: true
}}
scroll={{ x: 3000, y: 800 }}
columns={columns.map((one) => {
return {
...one,
render: (_, record) => {
return (
<CellBox
colInfo={one}
record={record}
setActive={setActive}
active={active}
editKey={editKey}
setEditKey={setEditKey}
dataSource={dataSource}
setDataSource={setDataSource}
/>
);
}
};
})}
search={false}
dateFormatter="string"
headerTitle="表格标题"
/>
);
};
- CellBox
import "./cellbox.less";
import { Input } from "antd";
function CellBox(props: any) {
const {
record,
colInfo,
setActive,
active,
setEditKey,
editKey,
setDataSource,
dataSource
} = props;
const onlyKey = `${colInfo.dataIndex}_${record.key}`;
function onClickKey() {
setActive(onlyKey);
}
console.count(); // 打印计数
function onDoubleClick() {
setEditKey(onlyKey);
}
function onEditSave(e) {
const value = e.target.value;
const dataSourceEdit = dataSource.map((one) => ({
...one,
[colInfo.dataIndex]:
one.key === record.key ? value : one[colInfo.dataIndex]
}));
setDataSource(dataSourceEdit);
setEditKey("");
}
const activeCom = (
<div
onClick={onClickKey}
onDoubleClick={onDoubleClick}
className={active === onlyKey ? "activeBox" : "cellBox"}
>
{record?.[colInfo?.dataIndex]}
</div>
);
return editKey === onlyKey ? (
<Input
defaultValue={record?.[colInfo?.dataIndex]}
onPressEnter={onEditSave}
/>
) : (
activeCom
);
}
export default CellBox;
1.3 运行效果
点击一个单元格,每个单元格都重新渲染了,在CellBox里打印发现渲染了多次
2. 优化表格效果
优化分析
2.1 链式操作修改
- 在子组件内部进行状态管理
- 声明两个变量存储,用于存储当前操作对象,和上一个的操作对象
- 暴露当前操作dom的方法,取消选中和取消编辑的方法操作方法
子组件修改如下
import "./cellbox.less";
import { Input } from "antd";
import { useState, useEffect, useRef } from "react";
// ++新增全局存储上一个存储对象
let preOperateNode = { current: null };
function CellBox(props: any) {
const [active, setActive] = useState(false);
const [editKey, setEditKey] = useState("");
const { record, colInfo, setDataSource, dataSource } = props;
const curOperateNode = useRef(null);
// +++给当前对象对象暴露操作dom的方法
useEffect(() => {
const clear = () => {
setActive(false);
};
const clearForm = () => {
setEditKey("");
};
curOperateNode.current = {
clear,
clearForm
};
}, []);
const onlyKey = `${colInfo.dataIndex}_${record.key}`;
// 操作当前时,取消之前的选中,更新上一个操作为当前操作
function onClickKey() {
if (
preOperateNode.current &&
preOperateNode.current !== curOperateNode.current
) {
preOperateNode?.current?.clear();
}
setActive(true);
preOperateNode.current = curOperateNode.current;
}
console.count();
// 操作当前时,取消之前的编辑状态,更新上一个操作为当前操作
function onDoubleClick() {
if (
preOperateNode.current &&
preOperateNode.current !== curOperateNode.current
) {
preOperateNode?.current?.clearForm();
}
setEditKey(onlyKey);
preOperateNode.current = curOperateNode.current;
}
function onEditSave(e) {
const value = e.target.value;
const dataSourceEdit = dataSource.map((one) => ({
...one,
[colInfo.dataIndex]:
one.key === record.key ? value : one[colInfo.dataIndex]
}));
setDataSource(dataSourceEdit);
setEditKey("");
}
const activeCom = (
<div
onClick={onClickKey}
onDoubleClick={onDoubleClick}
className={active ? "activeBox" : "cellBox"}
>
{record?.[colInfo?.dataIndex]}
</div>
);
return editKey === onlyKey ? (
<Input
defaultValue={record?.[colInfo?.dataIndex]}
onPressEnter={onEditSave}
/>
) : (
activeCom
);
}
看下效果,只会更新操作的两个 dom 的数据
2.2 渲染优化
- 使用shouldComponentUpdate处理组件重复渲染
class CellBoxComUpdate extends Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.value !== nextProps.value;
}
render() {
return <CellBox {...this.props} />;
}
}
export default CellBoxComUpdate;
- 打印数据如下,只有 value 发生变化时,页面才进行了刷新
2.3 可视化区域动态渲染
- 仅展示在可视区域的内容
- 使用节流函数进行处理,减少滚动的闪烁
export const VisibilityObserver = ({
children,
callback
}: {
children: ReactNode;
callback?: (isVisible: boolean) => void;
}) => {
const [isVisible, setIsVisible] = useState(false);
const [throttleHandleRowVisible] = useState(() => throttle(setIsVisible, 100));// 使用节流函数
const divRef = useRef<HTMLDivElement | null>(null);
const observerRef = useRef<any>(null); // Intersection Observer 的引用
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const [entry] = entries;
throttleHandleRowVisible(entry.isIntersecting);
});
if (divRef.current) {
observer.observe(divRef.current);
}
observerRef.current = observer;
return () => {
if (divRef.current) {
observer.unobserve(divRef.current);
}
if (observerRef.current) {
observerRef.current.disconnect(); // 销毁 Intersection Observer 实例
}
};
}, []);
return (
<div
style={{
width: '100%',
height: '100%'
}}
ref={divRef}
>
{isVisible && children}
</div>
);
};
- 初始化时只渲染当前可视区域的内容
- 滚到可视区域后,逐渐显示元素
3.完整优化测试代码
转载自:https://juejin.cn/post/7276257954297479220