• 客户端性能优化实践


    背景

    双十一大促时,客户客服那边反馈商品信息加载卡顿,在不断有订单咨询时,甚至出现了商品信息一直处于加载状态的情况,显然,在这种高峰期接待客户时,是没法进行正常的接待工作的。
    起初,页面一直处于加载状态,初步认为是后端接口返回太慢导致,后经过后端日志排查,发现接口返回很快,根本不会造成页面一直处于加载状态,甚至出现卡死的状态。后经过不断排查,发现是客户端性能问题导致。

    优化前

    咨询订单时,只咨询一条订单,用时需要3秒左右,当连续咨询5、6条订单时,用时甚至达到了一分多钟,仅仅5、6条订单竟然用时这么久,那么在持续不断有订单咨询时,页面就会出现一直加载,甚至卡死的状态,明显存在很大的性能问题。
    在这里插入图片描述

    在这里插入图片描述

    利用performance工具可以分析主线程的Event Loop,图中标出的Main就是主线程。
    主线程是不断执行 Event Loop 的,可以看到有很多个 Task(宏任务),当主线程中的任务过多时,会导致主线程长时间被占用,无法及时响应用户的交互操作,从而影响用户体验。这种情况下,页面可能会出现卡顿、延迟响应等问题。

    优化后

    当只咨询一条订单时,用时需要1秒时间,连续咨询5、6条订单,用时优化到只需要3秒时间,并且页面流畅,对于用户体验上得到了明显的提升。
    在这里插入图片描述
    在这里插入图片描述

    可以看出long task 减少了很多。
    那么,如何来优化呢?请看下面的内容。

    优化点

    在合适的时机进行组件渲染

    在排查代码的过程中发现,很多本不该当前状态渲染的组件,都渲染出来了,显然这是不合理的。过多的组件渲染会占用大量的内存,并且也会增加页面的渲染时间,自然,响应性能就会变得很差,用户与页面的交互就会变得迟缓。
    而商品信息加载部分最常见的不必要的组件渲染表现在使用Modal弹窗时,我们都知道当visible为true时,会弹出弹窗相应的页面内容,但是当visible为false时,其实是不希望渲染Modal弹窗中的内容的,这会带来额外的性能开销。

    下面是一些示例:

    -  ...
    -  <Modal
    -   ...
    -   visible={editVisible}
    -   ...
       >
    -  ...
    -  </Modal>
    -  ...
    +  {editVisible && (
    +     <GoodsAttributeModal
    +      editVisible
    +      ...
    +     />
    +  )}
    // 把Modal弹窗作为一个单独组件提取出去,并且只有当editVisible为true时才渲染组件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第一段代码中,使用了visible={editVisible}来控制Modal组件的显示与隐藏。当editVisible为true时,Modal组件会被渲染出来,否则不会被渲染。

    第二段代码中,使用了条件渲染的方式,即通过{editVisible && …}来判断是否渲染Modal组件。当editVisible为true时,Modal组件会被渲染出来,否则不会被渲染。

    这两种方式的主要区别在于组件的渲染时机。在第一种方式中,Modal组件在每次渲染时都会被创建和销毁,而在第二种方式中,只有在editVisible为true时才会创建和渲染Modal组件。

    使用条件渲染的方式可以提高性能,特别是在组件层级较深或渲染频繁的情况下。因为只有在需要显示Modal组件时才会进行渲染,避免了不必要的组件创建和销毁,减少了内存消耗和渲染时间。

    总结起来,使用条件渲染的方式可以根据需要动态地控制组件的显示与隐藏,提高性能和用户体验。

    使用useCallback、useMemo、React.memo提升性能

    下面是一些示例:
    useCallback

    -  renderContent = (content, searchKey) => {
    -   if(content) {
    -     const contentWithBr = content.replace(/\↵/g, '
    '
    ).replace(/\n/g, '
    '
    ) - const regex = new RegExp(`(${searchKey})`, 'gi'); // 创建正则表达式,忽略大小写匹配 - const matches = content.match(regex) || []; // 匹配结果数组 - return ( - <React.Fragment> - {contentWithBr.split('
    '
    ).map((text, index) => ( - <React.Fragment key={index}> - {index > 0 && <br />} - {text.split(regex).map((subText, subIndex) => { - // console.log('subText',subText,matches) - return ( - <React.Fragment key={subIndex}> - {matches.includes(subText) ? ( - <span style={{ color: '#FF8800' }}>{subText}</span> - ) : ( - subText - )} - </React.Fragment> - ) - })} - </React.Fragment> - ))} - </React.Fragment> - ) - } else { - return '-' - } - } + const renderContent = useCallback((content, searchKey) => { + if (content) { + const contentWithBr = content.replace(/\↵/g, '
    '
    ).replace(/\n/g, '
    '
    ) + const regex = new RegExp(`(${searchKey})`, 'gi') // 创建正则表达式,忽略大小写匹配 + const matches = content.match(regex) || [] // 匹配结果数组 + return ( + <React.Fragment> + {contentWithBr.split('
    '
    ).map((text, index) => ( + <React.Fragment key={index}> + {index > 0 && <br />} + {text.split(regex).map((subText, subIndex) => { + //console.log('subText',subText,matches) + return ( + <React.Fragment key={subIndex}> + {matches.includes(subText) ? ( + <span style={{ color: '#FF8800' }}>{subText}</span> + ) : ( + subText + )} + </React.Fragment> + ) + })} + </React.Fragment> + ))} + </React.Fragment> + ) + } else { + return '-' + } + }, [])
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    上面的代码使用了React的useCallback钩子函数来定义了一个名为renderContent的函数。useCallback的作用是用来缓存函数,以便在依赖项不变的情况下避免函数的重新创建。

    使用useCallback的好处是可以优化性能,特别是在父组件重新渲染时,避免不必要的函数重新创建。当依赖项数组为空时,useCallback会在组件的初始渲染时创建函数,并在后续的渲染中重复使用同一个函数。

    而没有使用useCallback的情况下,每次组件重新渲染时都会创建一个新的renderContent函数,即使函数的实现逻辑完全相同。这可能会导致性能问题,特别是在组件层级较深或渲染频繁的情况下。

    因此,使用useCallback可以提高组件的性能,避免不必要的函数创建和内存消耗。但需要注意的是,只有在确实需要缓存函数并且依赖项不变的情况下才使用useCallback,否则可能会导致不必要的优化和错误。

    useMemo

    -  const tooltip = (
    -    <div>
    -      <h2>
    -        <span className={styles.title}>{title}</span>
    -        {
    -          !window.isVisibleGoods && (
    -            <span>
    -              {renderKnowledgeModal({
    -                label: '编辑',
    -                record: item,
    -               platGoodsId: plat_goods_id,
    -                classification_id: classificationId,
    -              })}
    -              <a
    -                className={styles.delete}
    -                onClick={() => handleDeleteKnowledage(item, classificationId)}
    -              >
    -                删除
    -              </a>
    -            </span>
    -          )
    -        }        
    -      </h2>
    -      <div className={styles.img_block}>{images}</div>
    -      <div
    -        className={classnames(styles.context, styles.tooltipsContext)}
    -        dangerouslySetInnerHTML={{ __html: ParseBrow.parse(context) }}
    -      />
    -    </div>
    -  )
    + const tooltip = useMemo(
    +    () => (
    +      <div>
    +        <h2>
    +          <span className={styles.title}>{title}</span>
    +          {!isVisibleGoods && (
    +            <span>
    +              {renderKnowledgeModal({
    +                label: '编辑',
    +                record: item,
    +                platGoodsId: plat_goods_id,
    +                classification_id: classificationId,
    +              })}
    +              <a
    +                className={styles.delete}
    +                onClick={() => handleDeleteKnowledage(item, classificationId)}
    +              >
    +                删除
    +             </a>
    +            </span>
    +          )}
    +        </h2>
    +        <div className={styles.img_block}>{images}</div>
    +        <div
    +          className={classnames(styles.context, styles.tooltipsContext)}
    +          dangerouslySetInnerHTML={{ __html: ParseBrow.parse(context) }}
    +        />
    +      </div>
    +    ),
    +    [
    +      title,
    +      renderKnowledgeModal,
    +      item,
    +      plat_goods_id,
    +      classificationId,
    +      images,
    +      context,
    +      handleDeleteKnowledage,
    +      isVisibleGoods,
    +    ]
    +  )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    在上面的代码中,使用了useMemo来缓存了一个变量tooltip的计算结果。这个计算结果是一个React元素,包含了一些子元素和事件处理函数等。通过将tooltip作为依赖数组的一部分,当依赖数组中的值发生变化时,useMemo会重新计算tooltip的值;如果依赖数组中的值没有发生变化,则直接返回上一次缓存的tooltip的值。

    这样做的好处是,当依赖数组中的值没有发生变化时,可以避免重复计算tooltip的值,提高组件的性能。而如果依赖数组中的值发生变化,useMemo会重新计算tooltip的值,确保tooltip的值是最新的。

    相比之下,如果不使用useMemo,每次组件重新渲染时都会重新计算tooltip的值,即使依赖数组中的值没有发生变化,这样会造成不必要的性能损耗。

    总结起来,使用useMemo可以优化组件的性能,避免不必要的计算。但是需要注意的是,只有在计算的成本比较高时才需要使用useMemo,否则可能会带来额外的开销

    React.memo

    -  export default Item
    +  import { isEqual } from 'lodash'
    +  export default React.memo(Item, isEqual)
    
    • 1
    • 2
    • 3

    export default Item 直接导出组件,每次父组件重新渲染都会重新渲染 Item 组件;
    而 export default React.memo(Item, isEqual) 使用 React.memo 进行包裹,并传入自定义的比较函数 isEqual,只有在 props 发生变化且通过 isEqual 函数比较不相等时才会重新渲染 Item 组件。
    注意:自定义的比较函数 isEqual 用于比较两个 props 是否相等。如果不传入比较函数,则默认使用浅比较(即 Object.is)来比较 props。如果传入了比较函数,则会使用该函数来比较 props。

    props解构变量时的默认值

    在这里插入图片描述

    在这段代码中,KnowledgeTab是一个使用了React.memo进行优化的组件。React.memo是一个高阶组件,用于对组件进行浅层比较,以确定是否需要重新渲染组件。当组件的props没有发生变化时,React.memo会返回之前渲染的结果,从而避免不必要的重新渲染。

    在KnowledgeTab组件中,knowledge_list是一个从props中解构出来的属性。而const knowledge_list_default = useMemo(() => [], [])是使用useMemo钩子函数创建的一个空数组。这样做的目的是为了在组件的初始渲染时,给knowledge_list一个默认值,以避免在解构时出现undefined的情况。

    如果直接使用knowledge_list=[]来给knowledge_list赋值,会破坏React.memo的优化。因为每次父组件重新渲染时,knowledge_list都会被重新创建,即使它的值没有发生变化。这样会导致KnowledgeTab组件的props发生变化,从而触发不必要的重新渲染。

    而使用useMemo创建一个空数组作为默认值,可以保证在父组件重新渲染时,knowledge_list_default的引用不会发生变化,从而避免不必要的重新渲染。这样就能够保持React.memo的优化效果,只有在knowledge_list的值真正发生变化时才会重新渲染KnowledgeTab组件。

    所以,总结起来就是默认值如果传给子组件,父组件每一次更新都会导致子组件更新,导致子组件的React.memo失效

    拆分为状态自治的独立组件

    当一个组件的代码变得复杂或包含大量的子组件时,可以考虑将其中的一部分代码抽取为一个独立的子组件。这样做的好处是可以将复杂的逻辑拆分为多个小组件,提高代码的可读性和可维护性。
    同时,抽取组件也可以配合使用React.memo进行优化。
    下面是一个抽取独立组件的例子
    在这里插入图片描述

    import React, { memo } from 'react'
    import { Tooltip } from 'antd'
    import classNames from 'classnames'
    import Item from './item'
    import styles from '../../index.less'
    
    interface Item {
      name: string
      id: string
    }
    interface CategoryProps {
      item: Item
      activeKey: string
      onClickItem: () => void
    }
    const Category: React.FC<CategoryProps> = props => {
      const { item, activeKey, onClickItem } = props
      const { name, id } = item
    
      return (
        <Tooltip
          title={name}
          placement="topRight"
          align={{
            offset: [0, 5],
          }}
        >
          <span
            key={id}
            className={classNames(styles.tab_item, {
              [styles.active_item]: activeKey === id,
            })}
            onClick={onClickItem}
          >
            {name}
          </span>
        </Tooltip>
      )
    }
    
    export default memo(Category)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
  • 相关阅读:
    Spring以及SpringBoot/SpringCloud注解
    从零开始的图像语义分割:FCN快速复现教程(Pytorch+CityScapes数据集)
    python基于django的高校奖学金管理系统
    华为OD机试 - 告警抑制 - 数据结构map(Java 2023 B卷 100分)
    Towards Interpretable Video Anomaly Detection 论文阅读
    终于有人把“Linux云计算路线”整理出来了,收藏起来,随时查看
    时间序列预测:用电量预测 05 BP神经网络
    010-JAVA一维数组与多维数组
    pytorch中gather函数的理解
    如何将视频转换成GIF动画表情包?
  • 原文地址:https://blog.csdn.net/qq_44586361/article/details/134479919