客户端性能优化实践

背景

双十一大促时,客户客服那边反馈商品信息加载卡顿,在不断有订单咨询时,甚至出现了商品信息一直处于加载状态的情况,显然,在这种高峰期接待客户时,是没法进行正常的接待工作的。

起初,页面一直处于加载状态,初步认为是后端接口返回太慢导致,后经过后端日志排查,发现接口返回很快,根本不会造成页面一直处于加载状态,甚至出现卡死的状态。后经过不断排查,发现是客户端性能问题导致。

优化前

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

利用performance工具可以分析主线程的Event Loop,图中标出的Main就是主线程。

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

优化后

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

可以看出long task 减少了很多。

那么,如何来优化呢?请看下面的内容。

优化点

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

在排查代码的过程中发现,很多本不该当前状态渲染的组件,都渲染出来了,显然这是不合理的。过多的组件渲染会占用大量的内存,并且也会增加页面的渲染时间,自然,响应性能就会变得很差,用户与页面的交互就会变得迟缓。

而商品信息加载部分最常见的不必要的组件渲染表现在使用Modal弹窗时,我们都知道当visible为true时,会弹出弹窗相应的页面内容,但是当visible为false时,其实是不希望渲染Modal弹窗中的内容的,这会带来额外的性能开销。

下面是一些示例:

javascript 复制代码
-  ...
-  <Modal
-   ...
-   visible={editVisible}
-   ...
   >
-  ...
-  </Modal>
-  ...
+  {editVisible && (
+     <GoodsAttributeModal
+      editVisible
+      ...
+     />
+  )}
// 把Modal弹窗作为一个单独组件提取出去,并且只有当editVisible为true时才渲染组件

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

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

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

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

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

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

下面是一些示例:
useCallback

javascript 复制代码
-  renderContent = (content, searchKey) => {
-   if(content) {
-     const contentWithBr = content.replace(/\↵/g, '<br>').replace(/\n/g, '<br>')
-      const regex = new RegExp(`(${searchKey})`, 'gi'); // 创建正则表达式,忽略大小写匹配
-      const matches = content.match(regex) || []; // 匹配结果数组
-      return (
-        <React.Fragment>
-          {contentWithBr.split('<br>').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, '<br>').replace(/\n/g, '<br>')
+      const regex = new RegExp(`(${searchKey})`, 'gi') // 创建正则表达式,忽略大小写匹配
+      const matches = content.match(regex) || [] // 匹配结果数组
+      return (
+        <React.Fragment>
+          {contentWithBr.split('<br>').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 '-'
+    }
+  }, [])

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

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

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

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

useMemo

javascript 复制代码
-  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,
+    ]
+  )

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

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

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

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

React.memo

javascript 复制代码
-  export default Item
+  import { isEqual } from 'lodash'
+  export default React.memo(Item, isEqual)

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进行优化。

下面是一个抽取独立组件的例子

javascript 复制代码
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)
相关推荐
我要洋人死16 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人28 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人28 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR34 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香36 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969338 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai44 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_9151 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
小牛itbull3 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress