react-grid-layout 中的懒加载性能优化
# react-grid-layout 中的懒加载性能优化
# 一、背景
项目中视图详情页面使用了 react-grid-layout 来布局可拖拽页面。
在视图详情中,可能有很多图表(未限制添加条件),图表占用了一定的页面空间,所以很多图表可能不显示在可见区域内,首次加载时或者刷新视图时会把整个视图下所有的图表接口数据都加载一次,对后端的数据请求有些压力。而在实际使用中, 用户可能都不会滚动页面去看网页下方看不见的其他图表,所以造成了接口的浪费。
# 技术栈
- react 全家桶
- react-grid-layout
# 二、分析
# 当前页面逻辑

# 优化思路
由于 react-grid-layout 的特性, 布局是一次性完成的,所以只能对图表内容方面进行懒加载优化,如何懒加载呢?
对! 采用常用做法,判断元素是否在可见区域。
初始状态:默认是一个加载中间态 loading
核心思想:
- 首次拿到视图详情接口后计算布局,根据布局信息展示图表占位符(初始状态)
- 在图表组件中判断当前图表是否在可见区域,可见的话就去请求该图表接口
- 计算图表信息,将初始状态更新为图表信息。

# 三、解决
有了解决思路, 剩下代码方面的就好解决了,我们使用伪代码来进行模拟验证。
# 图表布局
const BasicLayout = () => {
const defaultProps = {
className: "layout",
items: 20,
rowHeight: 30,
onLayoutChange: function () {},
cols: 12
};
const [layout, setLayout] = useState(null);
const generateDOM = () => {
const doms = _.map(_.range(defaultProps.items), function (i) {
return (
<div key={i} style={{ background: "#e5e5e5" }}>
<span className="text">{i}</span>
</div>
);
});
return doms;
};
const generateLayout = () => {
return _.map(new Array(defaultProps.items), function (item, i) {
const y = _.result(defaultProps, "y") || Math.ceil(Math.random() * 4) + 1;
return {
x: (i * 2) % 12,
y: Math.floor(i / 6) * y,
w: 2,
h: y * 4,
i: i.toString()
};
});
};
const onLayoutChange = (layout) => {
defaultProps.onLayoutChange(layout);
};
useEffect(() => {
setLayout(generateLayout());
}, []);
return (
<ReactGridLayout
layout={layout}
onLayoutChange={onLayoutChange}
{...defaultProps}
>
{generateDOM()}
</ReactGridLayout>
);
};
# 封装图表组件
import React from "react";
export const Item = ({ id, data }) => {
return (
<div key={id} style={{ background: "#e5e5e5" }}>
{!data && <span>loading</span>}
{data && <span className="text">{data}</span>}
</div>
);
};
# 增加监听组件
import React, { useCallback, useEffect, useRef } from "react";
import debounce from "lodash/debounce";
/**
* 检查当前组件是否在屏幕
* @param id 组件id
* @param onScroll 滚动事件处理
* @returns 组件
*/
export const ScrollModule = ({ id, onScroll, children }) => {
const domRef = useRef();
const height = document.body.clientHeight; // 获取屏幕高度
const checkVisible = debounce(
(height, id, domRef) => {
const top = domRef.current?.getBoundingClientRect().top;
if (top >= 0 && top <= height) {
console.log("可见" + id);
// do something
onScroll(id);
}
},
[200]
);
const bindHandleScroll = useCallback(() => {
checkVisible(height, id, domRef);
}, [height, id, domRef, checkVisible]);
useEffect(() => {
// 页面滚动监听
window.addEventListener("scroll", bindHandleScroll, true);
return () => {
// 注销页面滚动监听
window.removeEventListener("scroll", bindHandleScroll, true);
};
}, [bindHandleScroll]);
return (
<div ref={domRef} key={id} style={{ height: "100%" }}>
{children}
</div>
);
};
# 渲染组件更新(完整代码)
const BasicLayout = () => {
const defaultProps = {
className: "layout",
items: 20,
rowHeight: 30,
onLayoutChange: function () {},
cols: 12
};
const [layout, setLayout] = useState(null);
const [data, setData] = useState({});
const onScroll = useCallback(
(id) => {
if (data[id]) return;
data[id] = "chart";
setData({ ...data });
},
[data]
);
const generateDOM = useCallback(() => {
const doms = _.map(_.range(defaultProps.items), function (i) {
return (
<div key={i} style={{ background: "#e5e5e5" }}>
<ScrollModule id={i} onScroll={onScroll}>
<Item id={i} data={data[i]} />
</ScrollModule>
</div>
);
});
return doms;
}, [data, onScroll]);
const generateLayout = () => {
return _.map(new Array(defaultProps.items), function (item, i) {
const y = _.result(defaultProps, "y") || Math.ceil(Math.random() * 4) + 1;
return {
x: (i * 2) % 12,
y: Math.floor(i / 6) * y,
w: 2,
h: y * 4,
i: i.toString()
};
});
};
const onLayoutChange = (layout) => {
defaultProps.onLayoutChange(layout);
};
useEffect(() => {
setLayout(generateLayout());
}, []);
return (
<ReactGridLayout
layout={layout}
onLayoutChange={onLayoutChange}
{...defaultProps}
>
{generateDOM()}
</ReactGridLayout>
);
};
# 效果

# 总结
通过预设 loading 页面来增强用户体验优化;
通过页面可视区域的判断来决定接口的请求及后续渲染,能极大的避免后台接口及数据压力,增强了页面可用性;
通过数据缓存来避免重复数据的请求,减少网络开销;
上次更新: 2023/05/23, 08:21:48