内容简介:上一篇文章讲了 React 性能优化的一些方向和手段,这篇文章再补充说一下如何进行性能测量和分析, 介绍 React 性能分析的一些工具和方法.进行任何性能优化的前提是你要找出’性能问题‘,这样才能针对性地进行优化。我觉得对于 React 的性能优化可以分两个阶段:下面本文测试的样板代码.
上一篇文章讲了 React 性能优化的一些方向和手段,这篇文章再补充说一下如何进行性能测量和分析, 介绍 React 性能分析的一些 工具 和方法.
进行任何性能优化的前提是你要找出’性能问题‘,这样才能针对性地进行优化。我觉得对于 React 的性能优化可以分两个阶段:
-
1. 分析阶段
- 通过分析器(Profiler)找出重新渲染的组件、重新渲染的次数、以及重新渲染耗费的资源与时间
- 变动检测. 通过分析器我们可以知道'什么被重新渲染, 重新渲染的代价',那么变动检测回答的问题就是: ’为什么这些进行了重新渲染?'
-
2. 优化阶段. 优化阶段我们针对分析阶段抛出的问题进行解决,解决的方法有很多,可以参考本文的姊妹篇< 浅谈React性能优化的方向 >
本文大纲
下面本文测试的样板代码.
推荐点击 Preview 面板的 Open In New Window
, 或者直接点击该链接,在线动手实践
分析器
分析哪些组件进行了渲染,以及渲染消耗的时间以及资源。主要工具有 React 官方的开发者工具以及 Chrome 的 Performance 工具。
React Devtool
最先应该使用的肯定是官方提供的开发者工具,React v16.5 引入了新的 Profiler 功能,让分析组件渲染过程变得更加简单,而且可以很直观地查看哪些组件被渲染.
高亮更新
首先最简单也是最方便的判断组件是否被重新渲染的方式是'高亮更新(Hightlight Updates)'.
① 开启高亮更新:
② 运行效果如下:
③ 通过高亮更新,基本上可以确定哪些组件被重新渲染. 所以现在我们给 ListItem 加上 React.memo(查看 PureList 示例), 看一下效果:
效果非常明显,现在只有递增的 ListItem 会被更新,而且当数组 排序 时只有 List 组件会被刷新. 所以说‘纯组件’是 React 优化的第一张牌, 也是最有效的一张牌.
分析器
如果 高亮更新
无法满足你的需求,比如 你需要知道具体哪些组件被渲染、渲染消耗多少时间、进行了多少次的提交(渲染)等等 , 这时候就需要用到分析器了.
① 首先选择需要收集测量信息的节点(一般默认选中根节点,有一些应用可能存在多个组件树,这时候需要手动选择):
② Ok,点击 Record 开始测量
③ 看看测量的结果,先来了解一下 Profiler 面板的基本结构:
-
:one: 这是一个 commit 列表。commit 列表表示录制期间发生的 commit(可以认为是渲染) 操作,要理解 commit 的意思还需要了解 React 渲染的基本原理.
在 v16 后 React 组件渲染会分为两个阶段,即 render 和 commit 阶段。
- render 阶段决定需要进行哪些变更,比如 DOM 。顾名思义, 这个阶段 React 会调用 render 函数,并将结果和上一次 render 的结果进行 diff, 计算出需要进行变更的操作队列
- commit 阶段 。或者称为提交阶段, 在这个阶段会执行 render 阶段 diff 出来的变更请求。比如 DOM 插入、更新、删除、排序等等。在这个阶段 React 还会调用 componentDidMount 和 componentDidUpdate 生命周期函数.
在 v16 之前,或者在 Preact 这些'类 React' 框架中,并不区分 render 阶段和 commit 阶段,也就说这两个阶段糅合在一起,一边 diff 一边 commit。有兴趣的读者可以看笔者之前写的 从 Preact 中了解组件和 hooks 基本原理
切换 commit:
-
:two: 选择其他图形展示形式,例如
Ranked 视图
,这个视图按照渲染消耗时间对组件进行排序: -
:three: 火焰图这个图其实就是 组件树 ,Profiler 使用颜色来标记哪些组件被重新渲染。 和 commit 列表以及 Ranked 图一样,颜色在这里是有意义的,比如灰色表示没有重新渲染;从渲染消耗的时间上看的话:
黑色 > 黄色 > 蓝色
, 通过 :point_up_2:Ranked 图可以直观感受到不同颜色之间的意义 -
:four: 当前选中组件或者 Commit 的详情, 可以查看该组件渲染时的 props 和 state
双击具体组件可以详细比对每一次 commit 消耗的时间:
-
:five: 设置
另外可以通过设置,筛选 Commit,以及是否显示原生元素:
④ 现在使用 Profiler 来分析一下 PureList 的渲染过程:
关于 Profiler 的详细介绍可以看这篇官方博客< Introducing the React Profiler >
Chrome Performance 工具
在 v16.5 之前,我们一般都是利用 Chrome 自带的 Performance 来进行 React 性能测量:
React 使用标准的 User Timing API
(所有支持该标准的浏览器都可以用来分析 React)来记录操作,所以我们在 Timings 标签中查看 React 的渲染过程。React 还特意使用 emoji 标记.
相对 React Devtool 而言 Performance 工具可能还不够直观,但是它非常强大,举个例子, 如果说 React-Devtool 是Fiddler, 那么 Performance 就是Wireshark. 使用 Performance 可以用来定位一些比较深层次的问题,这可能需要你对 React 的实现原理有一定了解, 就像使用 Wireshark 你需要懂点网络协议一样
所以说使用 Performance 工具有以下优势:
- 可以测量分析整个渲染的过程细节. 它可以定位某些具体方法的调用过程和消耗, 方便定位一些深层次问题.
- 可以测量分析底层 DOM 的绘制、布局、合成等细节。方便定位浏览器性能问题
其实 Performance 是一个通用的性能检测工具,所以其细节不在本文讨论访问。 详细参考
其他工具
上面介绍的这些工具基本上已经够用了。社区上还有一些比较流行的工具,不过这些工具迟早/已经要被官方取代(招安),而且它们也跟不上 React 的更新。
- react-addons-perf React v16 不支持了,不说了。老版本可用
- react-perf-devtool 也不活跃了,不推荐使用
变动检测
OK, 我们通过分析工具已经知道我们的应用存在哪些问题了,诊断出了哪些组件被无意义的渲染。下一步操作就是找出组件重新渲染的元凶, 检测为什么组件进行了更新.
我们先假设我们的组件是一个’纯组件‘,也就是说我们认为只有组件依赖的状态变更时,组件才会重新渲染. 非纯组件没有讨论的意义,因为只要状态变更或父级变更他都会重新渲染。
那么对于一个’纯组件‘来说,一般会有下面这些因素都可能导致组件重新渲染:
- props + state 毫无疑问. 这里我们只需要关注 来源于外部的 props . 内部 state 变动一般是人为触发的,比较容易发现
- Mobx observable value . 如果访问了 mobx 传进来的响应式数据,就会建立一个状态依赖关系,这个相对于 props 和 context 来说是隐式的,检测它的变动我们可能需要利用 mobx 提供的一些工具
- Context 。 Context 的 value 的变更会强制重新渲染组件
props 变动检测
在上一篇文章中我就建议简化 props,简单组件的 props 的变更很容易预测, 甚至你肉眼都可以察觉出来。另外如果你使用 Redux,如果严格按照 Redux 的最佳实践,配合 Redux 的开发者工具,也可以很直观地判断哪些状态发生了变更。
如果你没办法满足以上条件,可能就得依赖工具了。之前有一个 why-did-you-update 的库, 很可惜现在已经没怎么维护了(旧版本可以使用它) 。这个库使用猴补丁(monkey patches)来扩展 React,比对检测哪些 props 和 state 发生了变化:
后面也有人借鉴 why-did-you-update 写了个 why-did-you-render . 不过笔者还是不看好这些通过猴补丁扩展 React 的实现,依赖于 React 的内部实现细节,维护成本太高了,跟不上 React 更新基本就废了.
如果你现在使用 hook 的话,自己手写一个也很简单, 这个 idea 来源于 use-why-did-you-update :
import { useEffect, useRef } from 'react'; export function useWhyDidYouUpdate(name: string, props: Record<string, any>) { // ⚛️保存上一个props const latestProps = useRef(props); useEffect(() => { if (process.env.NODE_ENV !== 'development') return; const allKeys = Object.keys({ ...latestProps.current, ...props }); const changesObj: Record<string, { from: any; to: any }> = {}; allKeys.forEach(key => { if (latestProps.current[key] !== props[key]) { changesObj[key] = { from: latestProps.current[key], to: props[key] }; } }); if (Object.keys(changesObj).length) { console.log('[why-did-you-update]', name, changesObj); } else { // 其他原因导致组件渲染 } latestProps.current = props; }, Object.values(props)); } 复制代码
使用:
const Counter = React.memo(props => { useWhyDidYouUpdate('Counter', props); return <div style={props.style}>{props.count}</div>; }); 复制代码
如果是类组件,可以在 componentDidUpdate
使用类似上面的方式来比较 props
mobx 变动检测
排除了 props 变更导致的重新渲染,现在来看看是否是 mobx 响应式数据导致的变更. 如果你们团队不使用 mobx,可以跳过这一节。
首先不管是 Redux 和 Mobx,我们都应该让状态的变动变得可预测. 因为 Mobx 没有 Redux 那样固化的数据变更模式,Mobx 并不容易自动化地监测数据是如何被变更的。在 mobx 中我们使用 @action
来标志状态的变更操作,但是它拿异步操作没办法。好在后面 mobx 推出了 flow
API:clap:。
对于 Mobx 首先建议开启严格模式, 要求所有数据变更都放在@action 或 flow 中:
import { configure } from 'mobx'; configure({ enforceActions: 'always' }); 复制代码
定义状态变更操作
import { observable, action, flow } from 'mobx'; class CounterStore { @observable count = 0; // 同步操作 @action('increment count') increment = () => { this.count++; }; // 异步操作 // 这是一个生成器,类似于saga的机制 fetchCount = flow(function*() { const count = yield getCount(); this.count = count; }); } 复制代码
Ok 有了上面的约定,现在可以在控制台(通过 mobx-logger)或者 Mobx 开发者工具 中跟踪 Mobx 响应式数据的变动了。
如果不按照规范来,出现问题会比较浪费时间, 但也不是没办法解决。Mobx 还提供了一个trace函数, 用来检测为什么会执行 SideEffect:
export const ListItem = observer(props => { const { item, onShiftDown } = props; trace(); return <div className="list-item">{/*...*/}</div>; }); 复制代码
运行效果(递增了 value 值):
Context 变更检测
Ok, 如果排除了 props 和 mobx 数据变更还会重新渲染,那么 100%是 Context 导致的,因为一旦 Context 数据变动,组件就会被强制渲染。笔者在 浅谈 React 性能优化的方向 提到了 ContextAPI 的一些陷阱。先排除一下是否是这些原因导致的.
现在并没有合适的跟踪 context 变动的机制,我们可以采取像上文的 useWhyDidYouUpdate
一样的方式来比对 Context 的值:
function useIsContextUpdate(contexts: object = {}) { const latestContexts = useRef(contexts); useEffect(() => { if (process.env.NODE_ENV !== 'development') return; const changedContexts: string[] = []; for (const key in contexts) { if (contexts[key] !== latestContexts.current[key]) { changedContexts.push(key); } } if (changedContexts.length) { console.log(`[is-context-update]: ${changedContexts.join(', ')}`); } latestContexts.current = contexts; }); } 复制代码
用法:
const router = useRouter(); const myContext = useContext(MyContext); useIsContextUpdate({ router, myContext, }); 复制代码
React Devtool 的 Interactions
这是 React Devtool 的一个实验性功能,Interactions 翻译为中文是‘交互’?这个东西目的其实就是为了跟踪‘什么导致了更新’,也就是我们上面说的变动检测。React希望提供一个通用的API给开发者或第三方工具,方便开发者直观地定位更新的原因:
上图表示在记录期间跟踪到了四个交互,以及交互触发的时间和耗时。因为还是一个Idea阶段,所以我们就挑选一些API代码随便看看:
/** 跟踪状态变更 **/ import { unstable_trace as trace } from "scheduler/tracing"; class MyComponent extends Component { handleLoginButtonClick = event => { // 跟踪setState trace("Login button click", performance.now(), () => { this.setState({ isLoggingIn: true }); }); }; // render ... } /** 跟踪异步操作 **/ import { unstable_trace as trace, unstable_wrap as wrap } from "scheduler/tracing"; trace("Some event", performance.now(), () => { setTimeout( wrap(() => { // Do some async work }) ); }); /** 跟踪初始化渲染 **/ trace("initial render", performance.now(), () => render(<Application />)); 复制代码
好了行文结束,如果觉得可以就点个 :+1: 吧
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- WWDC 2018:使用日志框架测量性能
- Hyperledger Caliper:测量区块链性能的工具
- 测量JavaScript函数的性能的简单方法及与其他方式对比
- Android Studio 3.0 利用 Android Profiler 测量应用性能
- Ethr:一款TCP、UDP和HTTP网络性能测量工具
- .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Agile Web Development with Rails 4
Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95
Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!
图片转BASE64编码
在线图片转Base64编码工具
HEX CMYK 转换工具
HEX CMYK 互转工具