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

大家好,我是柒八九。

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

上面的一些优化方式,无论使用何种前端框架(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

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

「类组件的生命周期」

「函数组件的渲染步骤」

如前所述,「提交区域的每个条形图代表一个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的配置和使用方式,让一些不易察觉的问题直观的显现出来,并通过针对某个组件进行放大处理,找到其渲染过长的原因,对其对症下药。然后,做到药到病除。

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

后记

「分享是一种态度」

参考资料:

原文地址:https://cloud.tencent.com/developer/article/2081825

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom