Bug 分析与解决-antdPro-formList->性能优化hooks(带源码篇)

What -> Why -> How -> Never

一、问题的描述

1. 问题的表现是怎样的?

  • 关于问题的表现的描述,素材应尽量1,多附上一些截图/录屏/复现文档

页面卡死

  1. 问题的复现路径
  • 用户的操作路径和时间线
  • QA 可必现的路径

项目:HT文本解析平台

模块:提取模型配置

复现场景:要素130条数据,点击基础模型/提取模型配置-规则-按钮。 页面卡死

3. 正常的预期是什么?

  • 正常的表现应该是怎么样,和 PM/UX 对齐

快速渲染,不影响用户使用。

4. 问题的影响范围

  • 版本、分支、时间、相关数据

要素130条数据,造成页面卡顿50s,之后才能正常使用,点击按钮才会有交互效果

二、问题产生的原因分析

  • 详细描述问题产生时代码的执行逻辑,多利用工具辅助说明(截图/流程图等);
  • 如果问题的复现路径有多条,应该列出多条路径各自的代码执行逻辑;
  • 梳理过程中可以主动排除掉一些干扰项/分支流程,只留下干货;
  • 接口传输异常
  • 接口数据重构,时间复杂度造成卡顿
  • 组件渲染次数变多,导致页面卡顿
  • antdPro组件内部渲染数据造成卡顿

排除法发现根源:form表单setData函数后,造成内部组件卡顿

三、问题的解决方案

  • 详细描述修复问题的技术方案、相关示意图、流程图、时序图
  1. 寻找组件是否预留的性能优化的api。(0)

  2. 寻找组件是否预留事件可以进行调整。(0)

  3. 更改源码(0)

  4. 使用仅有的api和函数口子,实现虚拟滚动效果(1)

    1. 外部创建一个父级div包裹formList组件,监听滚动事件。更新showMinIndex, showMaxIndex.
    2. 在render函数中基于原有的index,进行判断,原组件或者占位高度的空盒子。
    3. 占位高度盒子,最好有加载效果,提升用户体验。
    4. 虚拟滚动后,高度重合,防止跳。
    5. 监听事件使用节流函数,减少频繁更新的次数。
    6. 最好缓存多少个子组件
    7. 什么时机更新index比较好,尽量避免频繁更新。
  5. 优化样式,优化代码,封装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 和验收结论

  1. 首次渲染,1s内渲染完毕,并可以操作。
  2. 滚动渲染,1s内渲染完毕。
  3. 不影响表单正常修改和保存。
相关推荐
我爱李星璇2 分钟前
HTML常用表格与标签
前端·html
疯狂的沙粒6 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员22 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐24 分钟前
前端图像处理(一)
前端
程序猿阿伟31 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒33 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪42 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背44 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js