上一篇文章讲了 React 性能优化的一些方向和手段,这篇文章再补充说一下如何进行性能测量和分析, 介绍 React 性能分析的一些工具和方法.html
进行任何性能优化的前提是你要找出’性能问题‘,这样才能针对性地进行优化。我以为对于 React 的性能优化能够分两个阶段:react
1. 分析阶段git
2. 优化阶段. 优化阶段咱们针对分析阶段抛出的问题进行解决,解决的方法有不少,能够参考本文的姊妹篇<浅谈React性能优化的方向>github
本文大纲web
下面本文测试的样板代码.chrome
推荐点击 Preview 面板的
Open In New Window
, 或者直接点击该连接,在线动手实践数组
分析哪些组件进行了渲染,以及渲染消耗的时间以及资源。主要工具备 React 官方的开发者工具以及 Chrome 的 Performance 工具。浏览器
最早应该使用的确定是官方提供的开发者工具,React v16.5 引入了新的 Profiler 功能,让分析组件渲染过程变得更加简单,并且能够很直观地查看哪些组件被渲染.性能优化
首先最简单也是最方便的判断组件是否被从新渲染的方式是'高亮更新(Hightlight Updates)'.markdown
① 开启高亮更新:
② 运行效果以下:
③ 经过高亮更新,基本上能够肯定哪些组件被从新渲染. 因此如今咱们给 ListItem 加上 React.memo(查看 PureList 示例), 看一下效果:
效果很是明显,如今只有递增的 ListItem 会被更新,并且当数组排序时只有 List 组件会被刷新. 因此说‘纯组件’是 React 优化的第一张牌, 也是最有效的一张牌.
若是高亮更新
没法知足你的需求,好比你须要知道具体哪些组件被渲染、渲染消耗多少时间、进行了多少次的提交(渲染)等等, 这时候就须要用到分析器了.
① 首先选择须要收集测量信息的节点(通常默认选中根节点,有一些应用可能存在多个组件树,这时候须要手动选择):
② Ok,点击 Record 开始测量
③ 看看测量的结果,先来了解一下 Profiler 面板的基本结构:
1️⃣ 这是一个 commit 列表。commit 列表表示录制期间发生的 commit(能够认为是渲染) 操做,要理解 commit 的意思还须要了解 React 渲染的基本原理.
在 v16 后 React 组件渲染会分为两个阶段,即 render 和 commit 阶段。
在 v16 以前,或者在 Preact 这些'类 React' 框架中,并不区分 render 阶段和 commit 阶段,也就说这两个阶段糅合在一块儿,一边 diff 一边 commit。有兴趣的读者能够看笔者以前写的从 Preact 中了解组件和 hooks 基本原理
切换 commit:
2️⃣ 选择其余图形展现形式,例如 Ranked 视图
,这个视图按照渲染消耗时间对组件进行排序:
3️⃣ 火焰图 这个图其实就是组件树,Profiler 使用颜色来标记哪些组件被从新渲染。和 commit 列表以及 Ranked 图同样,颜色在这里是有意义的,好比灰色表示没有从新渲染;从渲染消耗的时间上看的话: 黑色 > 黄色 > 蓝色
, 经过 👆Ranked 图能够直观感觉到不一样颜色之间的意义
4️⃣ 当前选中组件或者 Commit 的详情, 能够查看该组件渲染时的 props 和 state
双击具体组件能够详细比对每一次 commit 消耗的时间:
5️⃣ 设置
另外能够经过设置,筛选 Commit,以及是否显示原生元素:
④ 如今使用 Profiler 来分析一下 PureList 的渲染过程:
关于 Profiler 的详细介绍能够看这篇官方博客<Introducing the React Profiler>
在 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 工具备如下优点:
其实 Performance 是一个通用的性能检测工具,因此其细节不在本文讨论访问。 详细参考
上面介绍的这些工具基本上已经够用了。社区上还有一些比较流行的工具,不过这些工具早晚/已经要被官方取代(招安),并且它们也跟不上 React 的更新。
OK, 咱们经过分析工具已经知道咱们的应用存在哪些问题了,诊断出了哪些组件被无心义的渲染。下一步操做就是找出组件从新渲染的元凶, 检测为何组件进行了更新.
咱们先假设咱们的组件是一个’纯组件‘,也就是说咱们认为只有组件依赖的状态变动时,组件才会从新渲染. 非纯组件没有讨论的意义,由于只要状态变动或父级变动他都会从新渲染。
那么对于一个’纯组件‘来讲,通常会有下面这些因素均可能致使组件从新渲染:
在上一篇文章中我就建议简化 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
排除了 props 变动致使的从新渲染,如今来看看是不是 mobx 响应式数据致使的变动. 若是大家团队不使用 mobx,能够跳过这一节。
首先不论是 Redux 和 Mobx,咱们都应该让状态的变更变得可预测. 由于 Mobx 没有 Redux 那样固化的数据变动模式,Mobx 并不容易自动化地监测数据是如何被变动的。在 mobx 中咱们使用@action
来标志状态的变动操做,可是它拿异步操做没办法。好在后面 mobx 推出了 flow
API👏。
对于 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 值):
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但愿提供一个通用的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 />)); 复制代码