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 慢上遠比用數據證實要有意義的多。

相關文章
相關標籤/搜索