客户端性能优化实践

这篇具有很好参考价值的文章主要介绍了客户端性能优化实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

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

优化前

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

客户端性能优化实践,前端,react.js,性能优化

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

优化后

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

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

优化点

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

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

下面是一些示例:

-  ...
-  <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

-  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

-  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

-  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解构变量时的默认值

客户端性能优化实践,前端,react.js,性能优化

在这段代码中,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进行优化。
下面是一个抽取独立组件的例子
客户端性能优化实践,前端,react.js,性能优化文章来源地址https://www.toymoban.com/news/detail-769157.html

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)

到了这里,关于客户端性能优化实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于Electron27+React18+ArcoDesign客户端后台管理EXE

    electron-react-admin 基于electron27整合vite.js+react18搭建桌面端后台管理程序解决方案。 前几天有分享electron27+react18创建跨平台应用实践,大家感兴趣可以去看看。 https://blog.csdn.net/yanxinyun1990/article/details/134047329 Electron-ReactAdmin支持 暗黑/白昼 两种主题。 技术栈 编辑器:vscode 使用技

    2024年02月08日
    浏览(36)
  • 获取客户端真实 IP 地址的最佳实践

    1. 业务上云带来性能收益 公司从去年全面推动业务上云,而以往 IDC 架构部署上,接入层采用典型的 4 层 LVS 多机房容灾架构,在业务高峰时期,扩容困难(受限于物理机资源和 LVS 内网网段的网络规划),且抵挡不住 HTTPS 卸载引发的高 CPU 占用。 而经过压力测试发现,使用

    2024年02月05日
    浏览(42)
  • Java web应用性能分析之客户端慢

    客户端慢的原因包括: 终端设备老化(手机、PAD、电脑年限久远、运行期间产生了很多垃圾未清除) 终端网络设备老化(路由器、交换机老化) 跟我们使用的手机一样,路由器也需要及时更新换代,否则硬件跟不上了,再高的宽带也带不来流畅的网速。其中传输协议对于路

    2024年04月24日
    浏览(23)
  • hbase优化:客户端、服务端、hdfs

    hbase优化 一.读优化 1.客户端: 2.服务器: 3.列簇:是否过多、 是否使用布隆过滤器:任何业务都应该设置Bloomfilter,通常设置为row就可以,除非确认业务随机查询类型为row+cf,可以设置为rowcol 是否设置ttl 4.hdfs优化: 二、写优化 是否需要写WAL?WAL是否需要同步写入 用批量

    2024年02月14日
    浏览(40)
  • TCP客户端及服务器端开发实践

    ① TCP客户端应用程序开发 ② TCP服务器端应用程序开发 客户端程序是指运行在用户设备上的程序,服务端程序是指运行在服务器设备上的程序,专门为客户端提供数据服务。那如何记忆呢? 主动发起建立连接请求的是客户端程序,等待接受连接请求的是服务端程序。 开发流

    2024年04月09日
    浏览(43)
  • 前端解决客户端不安全随机数

    前端项目在安全漏洞扫描的时候,爆出了客户端不安全随机数的问题,看了下代码是因为使用了 Math.random() 生成随机数造成的。 百度了一下,math.random()并不是真的随机数,而是伪随机数! 原因 Math.random() 函数是 JavaScript 内置的一个函数,它用于生成一个 0 到 1(包括0,不包括

    2024年02月16日
    浏览(33)
  • 企业内网终端安全无客户端准入控制技术实践

    终端无代理/无客户端准入控制技术因其良好的用户体验而倍受创新企业的青睐。无代理/无客户端准入控制技术,顾名思义,是一种在网络中对终端实施访问控制的方法,无需依赖特定的客户端软件。 不同于银行、医院等传统行业的终端准入控制需求, 芯片半导体、生物制药

    2024年02月12日
    浏览(32)
  • Netty理论与实践(二) 创建http客户端 服务端

    1. 使用echo服务器模拟http 通过上一篇文章中的echo服务器程序来模拟一次HTTP请求。 接收消息的代码如下: 我们通过postman直接访问echo服务器: 请求成功,echo服务器接收到了本次HTTP请求,控制台打印内容如下: 上面的原理很容易理解,postman通过tcp建立与服务器localhost:8001的连

    2024年02月16日
    浏览(26)
  • 【参数汇总】mysql服务端/客户端常见优化参数

    1、innodb_buffer_pool_size (innodb索引buffer pool缓冲区大小) 默认大小为128M, 官方推荐其配置为系统内存的 50% 到 75% 。 一般innodb_buffer_pool_size要结合以下两个参数来设置: innodb_buffer_pool_chunk_size 从 MySQL 5.7.5 开始,我们可以动态修改 InnoDB Buffer Pool 的大小。这个新特性同时也引入

    2024年02月04日
    浏览(33)
  • 关于ios客户端与web网页js交互

    iOS与JS的交互可以通过以下几种方式实现: 1.使用UIWebView或WKWebView加载Html页面并在其中添加Javascript脚本,通过WebView的代理方法捕获Javascript脚本发出的事件,从而实现与iOS原生代码的交互。 2.使用JSBridge框架,它提供了一种简单的方式在Javascript和iOS原生代码之间进行通信。

    2024年02月10日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包