What -> Why -> How -> Never
一、问题的描述
1. 问题的表现是怎样的?
- 关于问题的表现的描述,素材应尽量1,多附上一些截图/录屏/复现文档
页面卡死
- 问题的复现路径
- 用户的操作路径和时间线
- QA 可必现的路径
项目:HT文本解析平台
模块:提取模型配置
复现场景:要素130条数据,点击基础模型/提取模型配置-规则-按钮。 页面卡死
3. 正常的预期是什么?
- 正常的表现应该是怎么样,和 PM/UX 对齐
快速渲染,不影响用户使用。
4. 问题的影响范围
- 版本、分支、时间、相关数据
要素130条数据,造成页面卡顿50s,之后才能正常使用,点击按钮才会有交互效果
二、问题产生的原因分析
- 详细描述问题产生时代码的执行逻辑,多利用工具辅助说明(截图/流程图等);
- 如果问题的复现路径有多条,应该列出多条路径各自的代码执行逻辑;
- 梳理过程中可以主动排除掉一些干扰项/分支流程,只留下干货;
- 接口传输异常
- 接口数据重构,时间复杂度造成卡顿
- 组件渲染次数变多,导致页面卡顿
- antdPro组件内部渲染数据造成卡顿
排除法发现根源:form表单setData函数后,造成内部组件卡顿
三、问题的解决方案
- 详细描述修复问题的技术方案、相关示意图、流程图、时序图
-
寻找组件是否预留的性能优化的api。(0)
-
寻找组件是否预留事件可以进行调整。(0)
-
更改源码(0)
-
使用仅有的api和函数口子,实现虚拟滚动效果(1)
- 外部创建一个父级div包裹formList组件,监听滚动事件。更新showMinIndex, showMaxIndex.
- 在render函数中基于原有的index,进行判断,原组件或者占位高度的空盒子。
- 占位高度盒子,最好有加载效果,提升用户体验。
- 虚拟滚动后,高度重合,防止跳。
- 监听事件使用节流函数,减少频繁更新的次数。
- 最好缓存多少个子组件
- 什么时机更新index比较好,尽量避免频繁更新。
-
优化样式,优化代码,封装hooks
ini
import _ from 'lodash';
import type { Ref } from 'react';
import { useRef, useState, useEffect } from 'react';
/**
* 虚拟滚动列表hooks
* @param maxLength
* @returns
*/
export const useFormVirtualListHooks = (maxLength: number) => {
const formListTarget = useRef();
const maxCount = 10;
const [showIndexMap, setShowIndexMap] = useState({
minIndex: 0,
maxIndex: maxCount,
});
let acceptHiddenCount = 5;
const setData = _.throttle(setShowIndexMap, 30);
const formListScrollFn = _.throttle((e) => {
// 父级视口高度
const currentListHeight = e.target.offsetHeight;
// 子集总高度
const totalHeight = e.target.firstChild.offsetHeight;
// 卷曲高度
const scrollTop = e.target.scrollTop;
// 平均单个高度
const singleItemHeight = Math.floor(totalHeight / maxLength);
const everyHeight = singleItemHeight;
// 开始索引
const startIndex = Math.floor(scrollTop / everyHeight);
// 结束索引
const endIndex = Math.min(
Math.ceil(scrollTop / everyHeight) + Math.ceil(currentListHeight / everyHeight),
maxLength,
);
// 接受隐藏的数据
acceptHiddenCount = Math.floor(currentListHeight / everyHeight) * 1.5;
if (scrollTop < currentListHeight) {
setData({
minIndex: 0,
maxIndex: maxCount,
});
} else if (scrollTop + currentListHeight >= totalHeight) {
setData({
minIndex: maxLength - maxCount - 1,
maxIndex: maxLength - 1,
});
} else {
setData({
minIndex: Math.min(startIndex - acceptHiddenCount, maxLength - maxCount),
maxIndex: Math.min(endIndex + acceptHiddenCount, maxLength),
});
}
}, 100);
useEffect(() => {
if (formListTarget.current) {
formListTarget.current?.addEventListener('scroll', formListScrollFn);
}
return () => {
formListTarget.current?.removeEventListener('scroll', formListScrollFn);
};
}, [formListTarget, showIndexMap.minIndex]);
return {
targetRef: formListTarget,
showIndexMap,
};
};
业务代码:
javascript
function App() {
const { targetRef: formListTarget, showIndexMap } = useFormVirtualListHooks(
wrapperLib.formRef?.getFieldValue('handler_rule_s').length || 1,
);
return (<div ref={formListTarget} className={styles.formListDiv}>
<ProFormList
style={{ width: 'fit-content' }}
>
{(value, index, method) => {
if (showIndexMap.minIndex <= index && index <= showIndexMap.maxIndex) {
// 原业务组件
return (
<div className="formListItem">
...
</div>
);
} else {
// 占位高度空盒子
return (
<Spin spinning={true} className="blankDiv">
<div
style={{
height: '108px',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'center',
alignContent: 'center',
}}
/>
</Spin>
);
}
}}
</ProFormList>
</div>)
}
五、QA 验收结果
QA 补充,走查验收的用例/Checklist 和验收结论
- 首次渲染,1s内渲染完毕,并可以操作。
- 滚动渲染,1s内渲染完毕。
- 不影响表单正常修改和保存。