三个分析React组件性能的工具

121 阅读9分钟

前言

抛开 React 框架,先来看看原生JS的性能优化。在原生JS中,只有两种情况:

  1. 渲染进程阻塞:一次渲染太多DOM节点,导致页面无法响应用户交互,页面出现卡顿。
  2. JS进程阻塞:JS进程一次执行太大的任务,比如说处理十万条数据,导致页面无法响应用户交互

抽丝剥茧,上述两个原因是JS出现卡顿的本质原因。优化思路是避免渲染进程和JS进程过久占用。

React 性能优化

在React的项目中,由于React的底层设计,导致React性能差的原因只有一个,即短时间里大量的重渲染。主要有下面这些情况

  • 父组件重新渲染(父组件的 stateprops 改变)
  • 子组件的 props 发生变化
  • 父组件传递给子组件的对象、数组或函数引用发生变化
  • 使用 React.memoshouldComponentUpdate 来避免不必要的渲染
  • 列表项的 key 发生变化
  • 上下文值变化

下面是一些组件和Hooks工具,用来分析导致React组件重渲染的原因

React.Profiler

React.ProfilerReact 中的一个组件,用于测量 React 应用程序的性能。它可以帮助开发者找出性能瓶颈,了解组件的渲染时间、重新渲染的频率和渲染的成本,以便对应用程序进行优化。

使用 <Profiler> 包裹组件树,以测量其渲染性能。

<Profiler id="App" onRender={onRender}>  
  <App />  
</Profiler>

参数 

  • id:字符串,用于标识正在测量的 UI 部分。
  • onRenderonRender 回调函数,当包裹的组件树更新时,React 都会调用它。它接收有关渲染内容和所花费时间的信息。

下面重点讲解onRender方法,onRender方法接受的参数如下

function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime){ 
 // 对渲染时间进行汇总或记录...  
}

参数

  • id:字符串,为 <Profiler> 树的 id 属性,用于标识刚刚提交的部分。如果使用多个 profiler,可以通过此属性识别提交的是树中的哪一部分。
  • phase:为 "mount""update" 或 "nested-update" 中之一。这可以让你知道组件树是首次挂载还是由于 props、state 或 hook 的更改而重新渲染。
  • actualDuration:在此次更新中,渲染 <Profiler> 组件树的毫秒数。这可以显示子树在使用记忆化(例如 memo 和 useMemo)后的效果如何。理想情况下,此值在挂载后应显著减少,因为许多后代组件只会在特定的 props 变化时重新渲染。
  • baseDuration:估算在没有任何优化的情况下重新渲染整棵 <Profiler> 子树所需的毫秒数。它通过累加树中每个组件的最近一次渲染持续时间来计算。此值估计了渲染的最差情况成本(例如初始挂载或没有使用记忆化的树)。将其与 actualDuration 进行比较,以确定记忆化是否起作用。
  • startTime:当 React 开始渲染此次更新时的时间戳。
  • commitTime:当 React 提交此次更新时的时间戳。此值在提交的所有 profiler 中共享,如果需要,可以对它们进行分组。

结合笔者的使用经历,只有actualDuration比较有用,当渲染时间大于16ms会出现卡顿,根据actualDuration的值是否大于16ms决定是否改动代码。

假如某个组件的actualDuration的值远远大于16ms,那么怎么优化代码呢?请看下面讲述的whyDidYouRender方法,可以帮助你找出组件不必要的重新渲染的原因,提升性能

whyDidYouRender

whyDidYouRender 是一个工具方法,它可以帮助你找出组件不必要的重新渲染的原因。当组件发生重新渲染时,它会给你提供详细的信息,告诉你哪些 propsstate 的改变导致了重新渲染,以及这些改变是否真的必要,从而帮助你优化 React 应用的性能

React 中,子组件重渲染指的是一个组件在其父组件或状态发生变化时,重新执行该组件的渲染过程。React 会根据组件的 propsstate 是否发生变化来决定是否重新渲染该组件,而状态没有发生的组件也重新渲染会带来性能损失。

下面讲述如何在Vite项目中接入whyDidYouRender工具

1. 根目录安装whyDidYouRender工具

yarn add @welldone-software/why-did-you-render -D

2. 修改vite.config.js配置

export default defineConfig({
  plugins: [
    react({
      // 添加这一行代码
      jsxImportSource: "@welldone-software/why-did-you-render",
    }),
  ],
});

3. 添加 wdyr.js文件

import React from "react";
import whyDidYouRender from "@welldone-software/why-did-you-render";

whyDidYouRender(React, {
  trackAllPureComponents: true,
  trackExtraHooks: [],
  trackHooks: true,
  collapseGroups: true,
});

whyDidYouRender 接收的第二个参数是一个配置项,下面是各个配置项的解释:

include?: RegExp[]

  • 作用:指定要追踪的组件的正则表达式匹配列表。只有符合这些正则表达式的组件才会被追踪。
  • 用法:如果你只想追踪某些特定名称或类型的组件,可以通过此选项来设置。例如,追踪以 App 为前缀的组件:
include: [/^App/]

exclude?: RegExp[]

  • 作用:指定排除在外的组件的正则表达式匹配列表。即使某个组件本来符合 include 的规则,如果它符合 exclude 中的正则表达式,也不会被追踪。
  • 用法:排除某些不需要优化或调试的组件。例如,排除 Modal 组件:
exclude: [/Modal/]

trackAllPureComponents?: boolean

  • 作用:如果设置为 true,则会自动追踪所有通过 React.memo 包裹的纯组件,即那些没有改变 props 的组件。
  • 用法:默认为 false。如果你希望对所有纯组件进行追踪,启用此选项:
trackAllPureComponents: true

trackHooks?: boolean

  • 作用:如果设置为 true,则会追踪所有 React Hook 的变化,帮助你检测哪些 Hook 的值发生了变化,导致组件重新渲染。
  • 用法:默认为 true。启用此项可以帮助你优化 Hook 的使用:
trackHooks: true

logOwnerReasons?: boolean

  • 作用:如果设置为 true,会在控制台日志中显示导致组件渲染的父组件(owner)的原因。
  • 用法:用于分析是哪个父组件导致了某个子组件的多余渲染:
logOwnerReasons: true

trackExtraHooks?: Array<ExtraHookToTrack>

  • 作用:你可以在此数组中指定额外的自定义 Hook,why-did-you-render 会追踪这些 Hook 的变化。
  • 用法:适用于你希望追踪自定义 Hook 的情况。例如,追踪 useCustomHook
trackExtraHooks: [
  { hookName: 'useCustomHook', component: 'MyComponent' }
]

logOnDifferentValues?: boolean

  • 作用:如果设置为 true,只有当组件的 props 或 state 的值发生变化时,才会输出日志。即避免在组件的 props 没有变化时,依然输出日志。
  • 用法:用于减少日志输出,只有在组件实际发生变化时才会记录:
logOnDifferentValues: true

hotReloadBufferMs?: number

  • 作用:设置热加载缓冲时间(以毫秒为单位)。当你在开发中修改代码并进行热重载时,可能会有多个渲染事件。此配置可以延迟日志的输出,防止多次记录不必要的渲染。
  • 用法:例如,设置 100 毫秒缓冲:
hotReloadBufferMs: 100

onlyLogs?: boolean

  • 作用:如果设置为 true,只会输出日志,而不执行组件渲染的追踪操作。这有助于减少性能开销。
  • 用法:例如,只输出日志:
onlyLogs: true

collapseGroups?: boolean

  • 作用:如果设置为 true,当多个组件的渲染日志来源相同(例如,来自同一个父组件)时,它们会被折叠成一个组,从而减少日志的冗余显示。
  • 用法:例如,启用折叠:
collapseGroups: true

titleColor?: string

  • 作用:设置日志标题的颜色。
  • 用法:例如,设置为绿色:
titleColor: 'green'

diffNameColor?: string

  • 作用:设置组件名称差异(diff)的颜色。
  • 用法:例如,设置为红色:
diffNameColor: 'red'

diffPathColor?: string

  • 作用:设置组件路径差异(diff)的颜色。
  • 用法:例如,设置为蓝色:
diffPathColor: 'blue'

textBackgroundColor?: string

  • 作用:设置日志文本背景色。
  • 用法:例如,设置为淡黄色:
textBackgroundColor: 'lightyellow'

notifier?: Notifier

  • 作用:自定义通知器。如果你希望自定义日志的通知方式(例如发送通知、弹窗等),可以使用此选项。
  • 用法:提供一个 Notifier 实现,可以是一个对象或方法,用于通知渲染事件。
notifier: myCustomNotifierFunction

customName?: string

  • 作用:为 why-did-you-render 打印的日志添加一个自定义的名称(例如你的应用的名称或某个模块的名称),方便区分多个项目或模块的日志。
  • 用法:例如:
customName: 'MyApp'

这些配置项可以帮助你精细化控制 @welldone-software/why-did-you-render 的行为,优化性能追踪和调试。在开发阶段,根据项目的需要,可以灵活调整这些设置。

4. main.jsx中引入wdyr.js

// main.jsx

import './wdyr.js'

请看下面简单的示例

import "./App.css";

function A(props) {}

A.whyDidYouRender = true;

import Hooks from "./hooks";

function App() {
  const { count, increment, decrement, reset } = Hooks.useCounter();

  return (
    <>
      <A count={count} name={{}} />
      <h2>{count}</h2>
      <div>
        <button onClick={increment}>increment</button>
        <button onClick={decrement}>decrement</button>
        <button onClick={reset}>reset</button>
      </div>
    </>
  );
}

export default App;

App.whyDidYouRender = true;

运行效果如下

image.png

点击increment按钮,然后查看 console 输出日志

image.png

可以直接跟踪导致组件重渲染的原因,提供的信息非常详细

useTrackedEffect

useTrackedEffect 是ahook第三方库中的Hook,它主要是用来跟踪状态的变化并在状态发生变化时执行相应的副作用操作。与普通的 useEffect 不同,它能够精确地检测哪些状态发生了改变,从而避免了一些不必要的副作用执行,提高性能。

请看官方示例

import React, { useState } from 'react';
import { useTrackedEffect } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useTrackedEffect(
    (changes) => {
      console.log('Index of changed dependencies: ', changes);
    },
    [count, count2],
  );

  return (
    <div>
      <p>Please open the browser console to view the output!</p>
      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount((c) => c + 1)}>count + 1</button>
      </div>
      <div style={{ marginTop: 16 }}>
        <p>Count2: {count2}</p>
        <button onClick={() => setCount2((c) => c + 1)}>count + 1</button>
      </div>
    </div>
  );
};

页面上有两个按钮,绑定的业务逻辑一样,效果如下:

  • 点击第一个按钮,控制台输出[0]
  • 点击第二个按钮,控制台输出[1]

useTrackedEffect 接收依赖数组,如果依赖数组中哪些元素发生了变化,就会被useTrackedEffect侦测到,然后在控制台以数组的形式给出,数组的项是依赖数组的索引

总结

  • React.Profiler 测量组件渲染性能
  • whyDidYouRender 分析导致组件重渲染的原因
  • useTrackedEffect 分析组件内部哪些依赖变化