React-利用React-Profiler提升应用性能

1,397 阅读3分钟

焦虑是人面对虚无和自由时产生的一种眩晕

大家好,我是柒八九。

在前面的-性能优化系列中,我们通过网络和页面渲染的角度来阐述,如何针对一个页面进行优化提效。

上面的一些优化方式,无论使用何种前端框架(React/Vue)都适用,而今天,我们来讲讲如何使用React Profiler针对React项目进行性能分析和渲染提效。

老样子,话不多说,开始步入正题。

你能所学到的知识点

  1. React Profiler 的组成 推荐阅读指数 ⭐️⭐️⭐️
  2. 如何通过React Profiler查询并改正页面耗时操作 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️

你还在为得到一个组件的渲染次数渲染时间而发愁吗?

你还在使用console.log来计算这些重要的性能指标吗?

你还在为React性能优化而抓狂吗?

不要998,只要..... (走错片场了)重新来

解决以上令你“魂牵梦绕”的问题,React-Profiler你值得拥有。它足够老牌(2018年推出),它背景足够硬(有官方撑腰)

所以,总之就是要想React应用,变得丝滑,用它就对了。


案例实现

为了展示React Profiler,我们将有一个非常简单的应用程序。

  • 有一个自动生成的数字列表
  • 可以通过在文本框中输入的搜索词进行过滤

页面的整体结构 Filter/List

import { Chance } from 'chance';
const chance = new Chance();
// 生成一个长度为200,内容整数的随机数组
const items = Array.from(
  { length: 200 }, 
  () => `${chance.integer()}`
);

export const FilterableList = () => {
    const [searchTerm, setSearchTerm] = useState('');
    return <div className={'filterableList'}>
        <Filter onValueUpdated={setSearchTerm} />
        <List entries={items.filter(
                item => item.includes(searchTerm)
              )} 
        />
    </div>
}

组件List/ListItem的实现

export interface ListProps {
  entries: string[];
}

export const List: FC<ListProps> = ({entries}) => {
  return (
    <div className="list">
      {entries.map((value, index) => 
        <ListItem key={index} value={value}/>
      )}
    </div>
  );
}

interface ListItemProps {
    value: string;
}


export const ListItem: FC<ListItemProps> = 
({value}) => <div className={'item'}>{value}</div>

就是一个常规不能再常规的问题。一个长List,用于展示数据信息,一个输入框,用于检索列表信息。


React Profiler

我们假设,在你的浏览器环境下,已经安装了React-Dev-Tools的插件。如果没有,需要做一些额外的处理工作。如果能访问到谷歌商店,那就进行按照处理。如果不行的话,搜索react-devtools-extensions,然后按照指定的步骤进行操作。

一旦安装,React-Dev-Tools能够被任何使用React技术栈构建的网站所访问

React应用标签下,打开控制台,就会看到指定的插件信息。

针对页面的分析,我们需要先利用Profiler的录制功能,进行页面渲染过程的录制,然后才能对该渲染过程进行分析。

但是在开始录制之前,我们需要在Profiler启用一个重要的设置。点击右上角的齿轮图标。

ProfilerTab下,勾选第一个选项--记录每个组件渲染的原因

第二个选项(隐藏下面的提交)也很有用,特别是当你有很多commit,想过滤掉不重要的提交(那些低于某个阈值的commit)。


开始剖析

点击蓝色按钮,开始一个剖析工作。

或者,点击循环按钮使得重新加载页面并立即开始信息收录工作。

收录开始后,进行一些页面操作,然后点击红色按钮停止信息收录

对于测试案例,在文本框中输入111,然后一个一个地删除数字(111->11->1->'')。

停止收录后,得到的结果如下。


Profiler UI 界面

Profiler的UI界面在逻辑上可分为4个主要部分。

  1. 图表类型
    • 火焰图
    • 排序图
  2. 图表区域--在应用程序的剖析切片中,代表某次commit对应的组件渲染时间的相关信息。
  3. 提交区域--每个条形图代表应用程序在整个录制阶段所有的commit操作。每当你通过点击选择一个commit图表区域提交信息就会相应地更新。
  4. 提交信息面板--关于单个选定的commit阶段或单个选定组件的细节。

提交区域

React调和算法分为两个阶段:渲染提交

  • 渲染阶段收录组件进行何种的信息变更。在这个阶段,React 调用 render,然后将结果与之前的render进行比较( diff 算法)。
  • 提交阶段React将需要变更的一些列操作,更新到真正的DOM树上。

具体的实现细节,可以参考React-Fiber机制1/React-Fiber机制2

下面展示了,针对类组件和函数组件的渲染步骤。

:::: column ::: column-left

类组件的生命周期

::: ::: column-right

函数组件的渲染步骤

::: ::::

如前所述,提交区域的每个条形图代表一个commit,条形图越高,提交的时间越长。这些提交也可以通过一个从绿色到黄色的颜色梯度来区分

  • 黄色是性能较差的commit
  • 绿色是性能较好的commit

因此,较高的黄条代表commit时间比较短的绿条长


图表 - 火焰图

火焰图表示应用程序在特定commit中的渲染树。图表中的每一条都代表一个React组件。这些组件从上到下依次为根组件和叶子节点(根部是最上面的组件,叶子是最下面的)。

正如你所看到的,HeaderFilterableListApp的孩子,所以它们并排在第二行,而第一行是App

条形图的宽度表示该组件及其子组件的渲染时间
条形图的颜色代表组件本身渲染的时间(绿色代表快,黄色代表慢)

因此,在上面的例子中,FilterableList 的宽度代表 FilterableList及其孩子节点List的渲染时间。

另一方面,你可以看到FilterableList是绿色的,List是黄色的,这与数字相关--FilterableList只花了0.5ms渲染,List花了1.6ms渲染。

但如果在某次提交中,某个组件根本没有被渲染,会发生什么情况呢?

我们选择第四次commit的情况来分析。

AppHeader组件在过滤时不会改变,所以它们只在第一次commit时被渲染一次。在接下来的commit中,这两个组件都是灰色的,不过,它们看起来还是有点不同。

  • 灰色填充--在这次提交中没有渲染的组件,但它是渲染路径的一部分(例如,App没有渲染,但它是FilterableList的父组件,而FilterableList被渲染)。

  • 灰色渐变条纹--在本次commit中没有渲染的组件,也不是渲染路径的一部分(例如,Header没有渲染,但它也没有任何子代被渲染)。

同时,尽管App组件没有渲染,但它仍然有一个宽度。

所以,让我们把这个定义细化一下。

条形图

  • 宽度代表该组件最后一次被渲染时花费的时间
  • 颜色代表作为当前commit的一部分花费的时间

last but not least,你可以通过点击某个组件来放大缩小图表。

缩小组件 -- 从App整个commitFilter组件

放大组件-- 重新点击上层组件


图表 - 排序图

与火焰图类似,排序图表示一个单一的提交。然而,与火焰图不同的是,组件是按渲染时间而不是按渲染顺序排列的

这意味着,渲染时间最长的组件在最上面

另一个区别是,组件的条形宽度代表了该组件的渲染时间,不包括其子组件。这意味着颜色和宽度之间有直接的关联

正如你所看到的,List花了最长的时间来渲染,所以它位于顶部,它在条形图中是最宽的,它在条形图中是最黄的。

在这次commit过程中没有渲染的组件不会出现在排序图中

与火焰图类似,通过点击组件可以放大和缩小。


提交信息面板

提交信息面板有两种不同的用途。

  1. 展示整个应用的渲染信息

当没有选择任何组件时(放大),它会显示当前在commit过程中的commit概况。数据包括commit的时间(自应用程序启动以来),渲染的时间,以及优先级。

  1. 展示单个组件的渲染信息

当你在某个图表区域中点击一个组件(放大它)时,提交信息面板会显示这个组件的细节。这包括该组件在这个特定的commit过程中渲染的原因(如果你在设置中启用了这个选项,我们在刚开始的时候,有过介绍)以及带有时间戳的提交列表。这个列表是交互式的,允许你在这个特定组件参与的不同提交之间轻松浏览。


案例分析

现在我们已经熟悉了React Profiler,让我们看看如何将这些知识应用到实际开发中。

我们继续采用,文章开头的示例代码。

组件内部的逻辑是非常直接的,所以很难改进。

相反,我们将专注于渲染性能,尝试减少渲染次数。由于我们在commit之间所做的只是过滤,我们会假设item被渲染一次,然后在过滤操作后从DOM中移除。这意味着ListItem不应该在过滤时被渲染两次。然后,在我们提供的实验案例中,ListItem在每次commit的时候,都会被渲染。

让我们放大第二个commit中的一个ListItem,试着弄清楚。

放大后为我们提供了有用的信息--该item被重新渲染,因为它的propsvalue属性发生变化了。

为什么值会改变?因为,每次我们过滤列表时都会创建一个新的数组。由于我们使用item-index作为ListItem组件的键,每次我们改变过滤值时,对应的数据信息也会不同。

例如,在第一次渲染时,数组中的第一个item是用一个key=1的组件渲染的。然而,在第二次渲染时,当我们从数组中过滤掉一些值时,第一个item可能是不同的。React 会重新使用第一次渲染时的key=1的组件,但由于第一个item本身发生了变化,其内部包含的信息也发生了变化,因此要重新渲染。

为了解决这个问题,我们将在第一次创建数组时为数组中的每个item分配一个ID,并将其作为组件的键,而不是使用项目索引。

页面的整体结构 Filter/List

import { Chance } from 'chance';
const chance = new Chance();
// 生成一个长度为200,内容整数的随机数组
const items = Array.from(
  { length: 200 }, 
  (_, index) => ({ value: `${chance.integer()}`, id: index})
);

export const FilterableList = () => {
    const [searchTerm, setSearchTerm] = useState('');
    return <div className={'filterableList'}>
        <Filter onValueUpdated={setSearchTerm} />
        <List entries={items.filter(
                item => item.includes(searchTerm)
              )} 
        />
    </div>
}

组件List/ListItem的实现

export interface ListProps {
  entries: {value: string, id: number}[];
}

export const List: FC<ListProps> = ({entries}) => {
  return (
    <div className="list">
      {entries.map(({id, value}) => 
        <ListItem key={id} value={value}/>
      )}
    </div>
  );
}

interface ListItemProps {
    value: string;
}


export const ListItem: FC<ListItemProps> = 
({value}) => <div className={'item'}>{value}</div>

经过所谓的优化处理,在每次commit发生时,ListItem仍然会被重新渲染。

通过,查看提交信息面板中的渲染原因,发现是由于ListItems的父组件发生了渲染,导致了它也被重新渲染。而父组件重新渲染,是不管子组件内部的值是否发生变化。是一种强制性的渲染机制。

显然,这是一种不理想的渲染方式,而React也提供了一种规避这种无效渲染的方式-- React.memo

export const ListItem: FC<ListItemProps> = 
React.memo(({value}) => <div className={'item'}>{value}</div>)

经过React.memo处理后,在进行过滤操作,ListItems不会发生重新渲染了。

通过一个简单的例子展示了React-Profiler的配置和使用方式,让一些不易察觉的问题直观的显现出来,并通过针对某个组件进行放大处理,找到其渲染过长的原因,对其对症下药。然后,做到药到病除。

愿我们的应用,不在卡顿。


后记

分享是一种态度

参考资料:

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!