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. 不影响表单正常修改和保存。
相关推荐
Larcher3 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐15 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭27 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程