大家好,我是柒八九。
在前面的-「性能优化」系列中,我们通过网络和页面渲染的角度来阐述,如何针对一个页面进行优化提效。
- Web性能优化之延迟与带宽
- Web性能优化之Worker线程(上)
- Web性能优化之Worker线程(下)
- 性能优化之关键渲染路径
上面的一些优化方式,无论使用何种前端框架(React/Vue
)都适用,而今天,我们来讲讲如何使用React Profiler
针对React
项目进行性能分析和渲染提效。
老样子,话不多说,开始步入正题。
你能所学到的知识点
❝
React Profiler
的组成 「推荐阅读指数」 ⭐️⭐️⭐️- 如何通过
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
启用一个重要的设置。点击右上角的齿轮图标。
在Profiler
Tab下,勾选第一个选项--记录每个组件渲染的原因。
第二个选项(隐藏下面的提交)也很有用,特别是当你有很多commit
,想过滤掉不重要的提交(那些低于某个阈值的commit
)。
开始剖析
点击「蓝色」按钮,开始一个剖析工作。
或者,点击「循环按钮」使得「重新加载页面」并立即开始信息收录工作。
收录开始后,进行一些页面操作,然后点击「红色」按钮停止信息收录
对于测试案例,在文本框中输入111
,然后一个一个地删除数字(111->11->1->''
)。
停止收录后,得到的结果如下。
Profiler UI 界面
Profiler
的UI界面在逻辑上可分为4个主要部分。
- 「图表类型」
- 火焰图
- 排序图
- 「图表区域」--在应用程序的
剖析
切片中,代表某次commit
对应的组件渲染时间的相关信息。 - 「提交区域」--每个条形图代表应用程序在整个录制阶段所有的
commit
操作。每当你通过点击选择一个commit
,「图表区域」和「提交信息」就会相应地更新。 - 「提交信息面板」--关于单个选定的
commit
阶段或单个选定组件的细节。
提交区域
React调和算法分为两个阶段:「渲染」和「提交」。
- 「渲染阶段」收录组件进行何种的信息变更。在这个阶段,
React
调用render
,然后将结果与之前的render进行比较(diff
算法)。 - 「提交阶段」是
React
将需要变更的一些列操作,更新到真正的DOM树上。
具体的实现细节,可以参考React-Fiber机制1/React-Fiber机制2
下面展示了,针对类组件和函数组件的渲染步骤。
「类组件的生命周期」
「函数组件的渲染步骤」
如前所述,「提交区域的每个条形图代表一个commit
,条形图越高,提交的时间越长」。这些提交也可以通过一个从绿色到黄色的颜色梯度来区分
❝
- 黄色是性能较差的
commit
- 绿色是性能较好的
commit
❞
因此,「较高的黄条代表commit
时间比较短的绿条长」。
图表 - 火焰图
火焰图表示应用程序在「特定commit
中的渲染树」。图表中的每一条都代表一个React组件
。这些组件从上到下依次为根组件和叶子节点(根部是最上面的组件,叶子是最下面的)。
正如你所看到的,Header
和FilterableList
是App
的孩子,所以它们并排在第二行,而第一行是App
。
❝条形图的「宽度」表示该「组件及其子组件的渲染时间」 条形图的颜色代表组件「本身渲染的时间」(绿色代表快,黄色代表慢) ❞
因此,在上面的例子中,FilterableList
的宽度代表 FilterableList
及其孩子节点List
的渲染时间。
另一方面,你可以看到FilterableList
是绿色的,List
是黄色的,这与数字相关--FilterableList
只花了0.5ms渲染,List
花了1.6ms渲染。
但如果在某次提交中,某个组件根本没有被渲染,会发生什么情况呢?
我们选择第四次commit
的情况来分析。
App
和Header
组件在过滤时不会改变,所以它们只在第一次commit
时被渲染一次。在接下来的commit
中,这两个组件都是「灰色」的,不过,它们看起来还是有点不同。
- 「灰色填充」--在这次提交中没有渲染的组件,但它是「渲染路径的一部分」(例如,
App
没有渲染,但它是FilterableList
的父组件,而FilterableList
被渲染)。 - 「灰色渐变条纹」--在本次
commit
中没有渲染的组件,也不是渲染路径的一部分(例如,Header
没有渲染,但它也没有任何子代被渲染)。
同时,尽管App
组件没有渲染,但它仍然有一个宽度。
所以,让我们把这个定义细化一下。
❝「条形图」
- 「宽度」代表该组件最后一次被渲染时花费的时间
- 「颜色」代表作为当前
commit
的一部分花费的时间
❞
「last but not least」,你可以通过点击某个组件来「放大」或「缩小」图表。
「缩小组件」 -- 从App
整个commit
到Filter
组件
「放大组件」-- 重新点击上层组件
图表 - 排序图
与火焰图类似,排序图表示一个单一的提交。然而,与火焰图不同的是,组件是「按渲染时间而不是按渲染顺序排列的」。
这意味着,「渲染时间最长的组件在最上面」。
另一个区别是,「组件的条形宽度代表了该组件的渲染时间」,不包括其子组件。这意味着「颜色和宽度之间有直接的关联」。
正如你所看到的,List
花了最长的时间来渲染,所以它位于顶部,它在条形图中是最宽的,它在条形图中是最黄的。
「在这次commit
过程中没有渲染的组件不会出现在排序图中」。
与火焰图类似,通过点击组件可以放大和缩小。
提交信息面板
「提交信息面板」有两种不同的用途。
- 展示整个应用的渲染信息
当没有选择任何组件时(放大),它会显示当前在commit
过程中的commit
概况。数据包括commit
的时间(自应用程序启动以来),渲染的时间,以及优先级。
- 展示单个组件的渲染信息
当你在某个图表区域中点击一个组件(放大它)时,「提交信息面板」会显示这个组件的细节。这包括该组件在这个特定的commit
过程中「渲染的原因」(如果你在设置中启用了这个选项,我们在刚开始的时候,有过介绍)以及带有时间戳的「提交列表」。这个列表是交互式的,允许你在这个特定组件参与的不同提交之间轻松浏览。
案例分析
现在我们已经熟悉了React Profiler
,让我们看看如何将这些知识应用到实际开发中。
我们继续采用,文章开头的示例代码。
组件内部的逻辑是非常直接的,所以很难改进。
相反,我们将专注于渲染性能,尝试「减少渲染次数」。由于我们在commit
之间所做的只是过滤,我们会假设item
被渲染一次,然后在过滤操作后从DOM中移除。这意味着ListItem
不应该在过滤时被渲染两次。然后,在我们提供的实验案例中,ListItem
在每次commit
的时候,都会被渲染。
让我们放大第二个commit
中的一个ListItem
,试着弄清楚。
放大后为我们提供了有用的信息--该item
被重新渲染,因为它的props
中value
属性发生变化了。
为什么值会改变?因为,每次我们过滤列表时都会创建一个新的数组。由于我们使用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
的配置和使用方式,让一些不易察觉的问题直观的显现出来,并通过针对某个组件进行放大处理,找到其渲染过长的原因,对其对症下药。然后,做到药到病除。
愿我们的应用,不在卡顿。
后记
「分享是一种态度」。
参考资料:
- React-Fiber机制1
- React-Fiber机制2
- react-profiler