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

2022-08-25 15:32:00 浏览数 (1)

大家好,我是柒八九。

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

  • Web性能优化之延迟与带宽
  • Web性能优化之Worker线程(上)
  • Web性能优化之Worker线程(下)
  • 性能优化之关键渲染路径

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

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

你能所学到的知识点

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

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

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

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

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

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

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

案例实现

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

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

页面的整体结构 Filter/List

代码语言:javascript复制
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的实现

代码语言:javascript复制
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

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

「类组件的生命周期」

「函数组件的渲染步骤」

如前所述,「提交区域的每个条形图代表一个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

代码语言:javascript复制
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的实现

代码语言:javascript复制
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

代码语言:javascript复制
export const ListItem: FC<ListItemProps> = 
React.memo(({value}) => <div className={'item'}>{value}</div>)

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

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

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

后记

「分享是一种态度」

参考资料:

  • React-Fiber机制1
  • React-Fiber机制2
  • react-profiler

0 人点赞