React Hooks 究竟有多慢?

自从 Hooks 诞生以来,官方就有考虑到了性能的问题。添加了各类方法优化性能,好比 memo、hooks deps、lazy initilize 等。并且在官方 FAQ 中也有讲到,Function 组件每次建立闭包函数的速度是很是快的,并且随着将来引擎的优化,这个时间进一步缩短,因此咱们这里根本不须要担忧函数闭包的问题。javascript

固然这一点也经过个人实验证明了,确实不慢,不只仅是函数闭包不慢,就算是大量的 Hooks 调用,也是很是快的。简单来讲,1 毫秒内大约能够运行上千次的 hooks,也就是 useState useEffect 的调用。而函数的建立,就更多了,快的话十万次。java

不少人都以为既然官方都这么说了,那咱们这么用也就行了,不须要过度担忧性能的问题。我一开始也是这样想的。可是直到最近有一次我尝试对公司项目里面一个比较复杂的组件用 Hooks 重写,我惊奇的发现重渲染时间居然从 2ms 增加到了 4ms。业务逻辑没有任何变化,惟一的变的是从 Class 变成了 Hooks。这让我有点难以相信,我一直以为就算是慢也不至于慢了一倍多吧,怎么着二者差很少吧。因而我开始无差异对比两个写法的性能区别。react

懒人阅读指南

我相信确定不少懒人不想看下面的分析,想直接看结果。没问题,知足大家,直接经过目录找到最后看「总结」就行了,若是你以为有问题或者以为我说的不对,能够从新仔细阅读一下文章,帮我指出哪里有问题。redux

为何有这篇文章

其实我本来不是很想写一篇文章的,由于我以为这个只是很简单的一个对比。因而我只是在掘金的沸点上随口吐槽了两句,结果……我决定写一篇文章。主要是以为这群人好 two,就算是质疑也应该先质疑个人测量方式,而不是说个人使用方式。都用了这么多年了,还能用错)滑稽脸闭包

不过既然要写,就写的完备一些,尽可能把一些可能的状况都覆盖了,顺便问问你们是否有问题。若是你们对下面的测试方法或者内容有任何问题的话,请你们正常交流哦,千万不要有一些过激或者偏激的言论。由于性能测试这东西,一人一个方法,一人一个想法。dom

既然说道这里,其实有一点我要说,沸点里面说到的 50% 这个测量数据确实有些问题。主要有这么几个缘由,第一,我当初只是想抱着试试的心态,因而就直接在开发模式下运行的。第二,平时写代码写习惯了,就直接用了 Date.now() 而没有使用精度更高 performance.now() 从而致使了偏差略微有点大。虽然偏差略大,可是大方向仍是没错的函数

后文的测试中,我也将这些问题修复了,尽可能给你们一个正确的数据。布局

开始以前,咱们要知道……

假设如今有 HookCompClassComp 两个组件分别表示函数式组件和类组件,后文用 Hook(HC) 和 Class(CC) 代替。性能

功能定义

为了更加贴近实际,这里假设两个组件都要完成相同的一个功能。那就是用户登陆这个流程:测试

  • 有用户名输入框和密码输入框
  • 有一个登陆按钮,点击以后校验用户名是否为 admin 且密码为 admin
  • 若是校验成功,下方提示登陆成功,不然提示用户名或者密码错误
  • 每次输入内容,都将清空内容
  • 另外为了消除偏差,额外添加一个按钮,用于触发 100 次的 render,并 log 出平均的渲染时间。

DEMO

具体的业务逻辑的实现,请看后面的 DEMO 地址。

另外由于 Class 组件有 setState 能够自动实现 batch 更新,可是 Hook 不行,因此这里实现的时候把全部的更新操做都放在 React 事件中同步更新,众所周知,React 的事件是自带 batch 更新的,从而保证只有一次渲染。保证二者功能效果一致。

对比常量

  • 2018 款 15 寸 MacBook Pro 入门款,i7-8750H 6 核 12 线程 + 16g + 256g
  • Chrome Stable 79.0.3945.117
  • react 16.12.0 PS: 其实我从 16.8.0 就开始测试了,懒癌发做一直没有继续搞
  • react-dom 16.12.0

React 全家桶版本所有使用生产模式,下降开发模式的影响。

衡量标准:从函数调用到渲染到 DOM 上的时间

这个时间其实当组件量很是大的时候实际上是不许的,由于你们调用的时间是不一样的,可是渲染到 DOM 上的时间基本是一致的,就会致使在组件树越浅越前的组件测量出来的时间就会越长。可是这里的状况是页面只有一个对比组件,因此能够暂时用这个做为衡量标准。

针对 HC 来讲

  • 在组件运行的一开始就记录为开始时间

  • 使用 useLayoutEffect 的回调做为结束时间。该 Hook 会在组件挂载或者更新 DOM 以后同步调用。而 useEffect 会在下一个 tick 调用,若是使用该 hook 就会致使最终测量出来的结果广泛慢一些。

    function Hooks() {
    	const now = performance.now()
    	useLayoutEffect(() => console.log(performance.now() - now))
    	return (/* ui */)
    }
    复制代码

针对 CC 来讲

  • 当运行 render 方法的时候,记录时间
  • 当运行 componentDidUpdate 或者 componentDidMount 的时候,打印耗时。这两个钩子都是在组件挂载或者更新 DOM 以后同步调用,与 useLayoutEffect 调用时机一致。
class Class extends Component {
	componentDidMount = () => this.log()
	componentDidUpdate = () => this.log()
	log = () => console.log(performance.now() - this.now)
	render() {
		this.now = performance.now()
		return (/* ui */)
	}
}
复制代码

测试流程和结果计算

  • 页面刷新,此时要针对测试内容先进行 5 轮预热测试。目的是为了让 Chrome 对热区代码进行优化,达到最高的性能。
  • 每一轮包含若干次的渲染,好比 100 次或者 50 次,对于每一轮测试,都会抛弃 5% 最高和最低一共 10% 的数据,只保留中间的值,并对这些值计算平均值获得该轮测试结果
  • 而后进行 5 轮正常测试,记录每次的结果,统计平均值。
  • 将此时的值计算做为最终的数据值

DEMO 地址

PS: CodeSandBox 彷佛不能以生产模式运行,不过你能够将它一键部署到 ZEIT 或者 netlify 上面,查看生产环境的效果。

开胃菜-重渲染测试结果

最为开胃菜,用一个最多见的场景来测试实在是最合适不过了,那就是组件的重渲染。话说很少,直接上测试结果

Avg. Time(ms) Hook Hook(Self) Class Class(Self) Self Hook Slow
第一次平均时间 0.2546703217776267 0.04549450906259673 0.20939560484263922 0.02357143663115554 93.0069421499% 21.6216175927%
第二次平均时间 0.23439560331158585 0.045824176785382593 0.2072527365001676 0.02346153545019391 95.3161884168% 13.0965058748%
第三次平均时间 0.22417582970644748 0.04109888910674132 0.1931868181410399 0.022967028748858104 78.9473490722% 16.0409555184%
第四次平均时间 0.22082417118516598 0.04082417709159327 0.18879122377096952 0.02120880942259516 92.4868873031% 16.96739222%
第五次平均时间 0.22747252228577713 0.04126375367107627 0.1941208809532307 0.024725271102327567 66.8889837458% 17.1808623414%
五次平均时间 0.23231 0.0429 0.19855 0.02319 85.329% 16.981%

简单解释下数据,Hook 和 Class 是经过上面规定的方式统计出来的数据,而 Hook(Self) Class(Self) 是计算了 HC 和 CC 函数调用的时间,最后的 Self 和 Hook Slow 则是 Hook 相比 Class 慢的百分比。这里只须要关注不带 Self 的数据便可。

让咱们来细细「品味」一下,Hook 比 Class 慢了 16%。

等等??? 16%,emmm……乍一听这是一个多么惊人的数字,5 % 的性能下降都很难接受了,况且是 16%。若是你的页面中有上百个这样组件,想一想都知道……咦~~~那酸爽

Wait!!! 或许有人会说了,抛开数值大小谈相对值,这根本就是耍流氓么。每一个组件组件都是毫秒级别的渲染,这么小的级别做比较偏差也会很大。并且你的测试的测量方式真的很对么?为啥看到不少文章说 Hooks 性能甚至比 Class 组件还高啊。并且你这个测量真的准确么?

这里先回答一下测量的问题,上面也说了,useLayoutEffect 和 CDU/CDM 基本是一致的,并且为了佐证,这里直接上 Performance 面板的数据,虽然只能在开发模式下才能看到这部分数据,但依旧具备参考意义

Hooks

Class

固然由于我这里只是截取了一个样例,无法给你们一个平均的值,可是若是你们屡次尝试能够发现就算是 React 本身打的标记点,Class 也会比 Hook 快那么一点点。

而针对更多的疑问,这里咱们就基于这个比较结果,引伸出更多的比较内容,来逐步完善:

  • 挂载性能如何?也就是第一次渲染组件
  • 大量列表渲染性能如何?没准渲染的组件多了,性能就不会呈现线性叠加呢?
  • 当 Class 被不少 HOC 包裹的时候呢?

其余对比

挂载性能

经过快速卸载挂载 40 次计算出平均时间,另外将二者横向布局,下降每次挂载卸载的时候 Chrome Layout&Paint 上的差别。话很少说,直接上结果

Avg. Time(ms) Hook Hook(Self) Class Class(Self) Hook Slow(%)
第一次平均时间 0.5608108209295047 0.04027024968653112 0.5409459180727199 0.025810805980015446 3.6722530281%
第二次平均时间 0.6013513618224376 0.041216209128096294 0.5285134916571347 0.02486483395301007 13.7816482105%
第三次平均时间 0.5672973001728187 0.04797298587053209 0.5154054158845464 0.024729729252489837 10.0681682204%
第四次平均时间 0.5343243404216057 0.04378377736822979 0.5293243023491389 0.025405410073093465 0.9446076914%
第五次平均时间 0.5371621495263802 0.041081066671255474 0.5078378347428264 0.025540552529934292 5.774346214%
五次平均时间 0.56019 0.04286 0.52441 0.02527 6.848%

经过交替运行连续跑 5 轮 40 次测试,能够获得上面这个表格。能够发现,无论那一次运行,都是 Class 时间会少于 Hook 的时间。经过计算可得知,Hook 平均比 Class 慢了 (0.53346 - 0.49811) / 0.49811 = 7%,绝对差值为 0.03535ms。

这个的性能差距能够说是不多了,若是挂载上百个组件的时候,二者差距基本是毫秒内的差距。并且能够看出来,绝对值的差距能够说是依旧没有太多的变化,甚至略微微微微减小,能够简单的认为其实大部分的时间依旧都花费在一些常数时间上,好比 DOM。

大列表性能

经过渲染 100 个列表的数据计算出平均时间。

Avg. Time(ms) Hook Hook(500) Class Class(500) Hook Slow(%) Hook Slow(%,500)
第一次平均时间 2.5251063647026077 9.55063829376818 2.335000020313136 8.795957447604296 8.1415992606% 8.5798601307%
第二次平均时间 2.6090425597701934 9.59723405143682 2.3622340473168073 8.702127664211266 10.4480973312% 10.286063613%
第三次平均时间 2.5888297637488615 9.64329787530005 2.344893603684737 8.731808533218313 10.4028668798% 10.438723417%
第四次平均时间 2.567340426662184 9.604468084673615 2.334893631570517 8.76340427574642 9.95534837% 9.5974553092%
第五次平均时间 2.571702087694343 9.597553207756992 2.230957413012994 8.719042523149797 15.273472846% 10.075770158%
五次平均时间 2.5724 9.59864 2.3216 8.74247 10.844% 9.796%

咱们先不计算下慢了多少,先看看这个数值,100 次渲染一共 2ms 多,平均来讲一次 0.02ms,而而咱们上面测试的时候发现,单独渲染一个组件,平均须要 0.2ms,这中间的差距是有点巨大的。

而如何合理解释这个问题呢?只能说明在组件数小的时候,React 自己所用的时间与组件的时间相比来讲比例就会比较大,而当组件多了起来以后,这部分就变少了。

换句话说,React Core 在这中间占用了多少时间,咱们不得而知,可是咱们知道确定是很多的。

HOC

Hook 的诞生其实就是为了下降逻辑的复用,简单来说就是简化 HOC 这种方式,因此和 Hook 对线的实际上是 HOC。最简单的例子,Mobx 的注入,就须要 inject 高阶组件包裹才能够,可是对于 Hook 来说,这一点彻底不须要。

这里测试一下 Class 组件被包裹了 10 层高阶组件的状况下的性能,每一层包裹的组件作的事情很是简单,那就是透传 props。

啥?你说根本不可能套 10 层?其实也是很容易的,你要注意这里咱们所说的 10 层实际上是指有 10 层组件包裹了最终使用的组件。好比说你们都知道 mobx inject 方法或者 redux 的 connect 方法,看似只被包裹了一层,实际上是两层,由于还有一层 Context.Consumer。同理你再算上 History 的 HOC,也是同样要来两层的。再加上一些其余的东西,再加一点夸张不就够了,手动滑稽)

Avg. Time(ms) Class With 10 HOC
第一轮 0.2710439444898249
第二轮 0.2821977993289193
第三轮 0.278846147951189
第四轮 0.27269232207602195
第五轮 0.25384614182697546
五轮平均时间 0.27173

这结果也就是很清楚了吧,在嵌套较多 HOC 的时候,Class 的性能其实并很差,从 0.19855ms 增长到 0.27173ms,时间接近有 26% 的增长。而这个性能很差并非由于 Class,而是由于渲染的组件过多致使的。从另外一个角度,hook 就没有这种烦恼,即便是大量 Hook 调用性能依旧在可接受范围内。

量化娱乐一下?

有了上面的数据,来作一个有意思的事情,将数据进行量化。

假设有以下常数,r 表示 React 内核以及其余和组件数无关的常数,h 表示 hook 组件的常数,而 c 表示 Class 组件的常数,T 表示最终所消耗的时间。能够得知这四个参数确定不为负数。

经过简单的假设,能够获得以下等式:

T(n,m) = hn + cm + r
// n 表示 hook 组件的数量
// m 表示 class 组件的数量
复制代码

想要计算获得 r h c 参数也很简单,简单个鬼,由于数据不是准确的,不能直接经过求解三元一次方程组的方式,而是要经过多元一次拟合的方式求得,而我又不想装 matlab,因而千辛万苦找到一个支持在线计算多元一次方程的网站算了下,结果以下:

h = 0.0184907294
c = 0.01674766395
r = 0.4146159332
RSS = 0.249625719
R^2 = 0.9971412136
复制代码

这个拟合的结果有那么一点点差强人意,由于若是你把单个 Class 或者 Hook 的结果代入的话,会发现误差了有一倍多。因此我上面也说道只是娱乐娱乐,时间不够也无法细究缘由了。不过从拟合的结果上来看,也能发现一个现象,那就是 h 比 c 要大。

另外观察最后的拟合度,看起来 0.99 很大了,但实际上并无什么意义。并且这里数据选取的也不是很好,作拟合最好仍是等距取样,这样作出来的数据会更加准确。这里只是忽然奇想一想要玩玩看,因此就随便作了下。

总结

无论你是空降过来的仍是一点点阅读到这里的,我这边先直接说基于上面的结论:

  • 当使用 Hook 的时候,总体性能相比 Class 组件会有 10 - 20% 的性能下降。
  • 当仅仅使用函数式组件,而不使用 Hook 的时候,性能不会有下降。也就是说能够放心使用纯函数式组件
  • Hook 的性能下降不只仅体如今渲染过程,就算是在第一次挂载过程当中,也相比 Class 有必定程度的下降
  • Hook 的性能下降有三部分
    • 第一部分是 Hook 的调用,好比说 useState 这些。可是这里有一点须要注意的是,这里的调用指的是有无,而不是数量。简单来讲就是从 0 到 1,性能下降的程度远远高于 从 1 到 n。
    • 第二部分是由于引入 Hook 而不得不在每次渲染的时候建立大量的函数闭包,临时对象等等
    • 第三部分是 React 对 Hook 处理所带来的额外消耗,好比对 Hook 链表的管理、对依赖的处理等等。随着 Hook 的增长,这些边际内容所占用的时间也会变得愈来愈大。
  • 但 Hook 有一点很强,在逻辑的复用上,是远高于 HOC 方式,算是扳回一局。

因此 Hook 确实慢,慢的有理有据。但究竟用不用 Hooks 就全看,我不作定夺。凡事都有两面,Hooks 解决了 Class 一些短板,可是也引入了一些不足。若是必定要我推荐的话,我推荐 Hooks+Mobx。

Refs

One More

以上内容是我花了快一个月一点点整理出来的,甚至还跨了个不同凡响的「年」。性能测试自己就是一个颇有争议的东西,不一样的写法不一样的测试方式都会带来不一样的结果。我也是在这期间一点点修改个人测试内容,从最开始只有单组件测试,到后来添加了组件列表的测试,以及挂载的测试。另外对数据收集也修改了不少,好比屡次取平均值,代码预热等等。每一次修改都意味着全部测试数据要从新测试,但我只是想作到一个公平的对比。

就在如今,我依旧会以为测试里面有不少内容依旧值得去改进,可是我以为拖的时间太长了,并且我认为把时间花在从源码角度分析为何 Hook 比 Class 慢上远比用数据证实要有意义的多。

相关文章
相关标签/搜索