(How to greatly improve your React app performance)- Noam Elboim / from mediumcss
本文旨在總結常見的性能缺陷,以及如何來避免這些缺陷。html
(本文所涉及的示例源碼 請到譯者github下載)
性能問題在web應用開發中不是什麼新鮮事。vue
咱們每一個人都有這樣的時刻,當你把一個新的Component組件放到你的app中,你會忽然發現你嘗試的每個用戶交互動做都與指望的效果有很明顯的滯後。有時,你能夠重複使用多個一樣的組件,這種尷尬的動效滯後會更加明顯。像下面這樣:node
在那一刻你也許內心已經給寫這個組件的人起了好幾個綽號了。可是最好的辦法是:作些什麼,是的,你能夠的!react
咱們將重點解決如下幾個常見的 React 性能問題:git
1.錯誤的 shouldComponentUpdate 實現 ,爲何 PureComponent 沒能拯救你。github
2.太快的改變 DOM。web
3.濫用事件(events)和 回調(callbacks)。chrome
對於以上的每一個問題,咱們先解釋問題的根源,而後咱們提出一些簡單易用的方法來避免它。api
組件的component 鉤子函數 shouldComponentUpdate 的本意是用來阻止一些非必需的渲染(render), shouldComponentUpdate將即將更新的props和state做爲參數,若是返回值是true, render函數就執行,不然不執行 render.
React.Component 默認的實現 shouldComponentUpdate 是返回true.
越多的render渲染意味着耗費越多的時間。因此咱們須要防止沒必要要的更新來減小額外的時間。
爲此,你會想到咱們應該在實現 shouldComponentUpdate 的時候更謹慎些。
讓咱們看一個簡單的使用 shouldComponentUpdate 的例子:
等下,爲何不起做用呢?
不起做用是由於 React 每次渲染的時候建立了一個新的 ReactElement!
這就意味着 在 shouldComponentUpdate函數中 Shallow Comparision 如:return this.props.children !== nextProps.children;幾乎就至關於return true;
根據個人經驗,大多數組件一般都以某種方式支持 ReactElement props(PropTypes.node or PropTyps.elemtn)好比像children這是很常見的狀況。
那麼, PureComonent又是怎樣的呢?
React.PureComponent 是React.Component 的另一種方式。它不是總在其 shouldComponent 實現中返回true,而是 props 和 state 的淺層比較。
使用 PureComponent 會返回一樣的結果,以下:
這是 PureComponent 特性的bug嗎?我不肯定。咱們須要知道的是,PureComponent 在大多數狀況下不起做用,它並不能阻止一些沒必要要的更新。
咱們第一點想到的是——進行深度比較! 這確實管用,可是它有兩個重要缺陷:
1. 運行深度比較自己是一個過程比較長,比較重,比較耗時的動做。所以,在 shouldComponent 函數運行結束以後,render 函數才能運行。這樣一來性能非但不能提高反而會變得更差。
2. 這只是基於當前的 React Elements 實現,在將來版本中可能會取消。
綜上,在我看來,使用深度比較並非一個好的解決辦法。
爲了尋找到更好的解決方案,我研究了一些其餘的虛擬 DOM 庫,看看他們是怎麼解決這個問題的。
我發現了 Vue 做者Evan You 一個關於在Vue.js中添加 類React shouldeComponent 的 feature request 發表的一個有意思的評論。他解釋到,這個問題並不能經過 "diffing" 虛擬DOM解決,由於它有不少未知的問題。依賴 React Elements 來檢測組件中的狀態變化並非一個可行的解決方案。
在實際應用中,不該該在 shouldComponentUpdate 的實現中使用 React Elements 的比較做爲返回結果。相反,應該使用某種狀態的改變來告訴組件是否應該更新。
咱們應該基於prop的不一樣來通知 state 的改變,而不是經過使用this.props.children !== nextProps.children。最好是一個數字或者字符串,這樣比較會更快。
咱們甚至可使用一個新的 prop 專門用來通知組件是否應該更新。
更進一步,我和個人同事建立了一個高階組件(HOC)。這個組件使用繼承反轉(Inheritance Inversion)來擴展通用的 shouldComponent 實現,也是 PureComponent 的替代方案。 並且確實有效。代碼在這裏:
必須說明的是,這只是一個通用的實現,因此並非適用全部的狀況。具體能夠參考這裏
例子在這裏,使用了一個自定義的 shouldComponentUpdate 實現。正如上面提到的,它確實不會再進行沒必要要的渲染了。
幾種比較:
你是否在你的應用中屢次使用相同的組件,導致你的應用很是重動畫也很卡頓,有時候即便使用一個也會致使應用性能的損耗?
在建立複雜的組件時,你可能須要執行一些自定義 DOM 的操做。在建立的時候你可能會遇到兩個問題:
1. 觸發太多佈局(Layout)而沒有使用觸發複合(Composite)或者重繪(Paint)
2. 太多不必的Layout.屢次讀寫DOM,致使 DOM沒必要要的從新計算。
讓咱們看下 原生 Collapse 組件,在0和內容高度之間改變它的高度。點擊查看
當使用一個這樣的組件時,能夠正常展現。可是當你屢次使用的時候......
若是你不是在移動設備上查看,可能感受不明顯。須要將你的chrome performance選項調到 6x slowdown
讓咱們分析下 Collapse 組件發生了什麼——這是高度改變的時候的代碼:
這裏有兩個問題須要注意:
1. 咱們改變的height屬性,根據csstriggers.com這個列表,改變高度(height)觸發了佈局(Layout)的從新計算。若是咱們設法改變相似transform的東西,那隻會觸發Composite,而且會更平滑些,對嗎?
事實正式如此,這樣會表現更好,可是這樣就會在Collapse組件下留下一個空白,由於咱們沒有改變它的高度。
2. 上面代碼的第三行,這是常見的改變高度出發Layout的濫用:咱們從DOM讀取了高度this.contentEl.scrollHeight而後又經過this.containerEl.style.height對DOM設置了高度,而後屢次重複這樣的操做。
若是咱們能夠成組的一次性讀取過來高度,而後再一次性設置高度,這樣不是更好嗎?
批量的讀寫 DOM 是一個很好的減小 Layout 的嘗試。咱們可使用requestAnimationFrame對DOM 讀寫進行批量處理,像下面這樣:
requestAnimationFrame能保證你的代碼在瀏覽器下一幀觸發,減小頁面繪製成本,按需批量繪製。讓你的動畫更流暢。點擊查看具體實現
這樣用起來可能比較麻煩,那麼可使用內置組件或者使用第三方庫好比Fastdom, Fastdom也是基於requesAnimationFrame 的原理經過批量處理DOM 讀取/寫入 操做來消除頻繁的Layout操做。
值得一提的是,因爲瀏覽器和設備功能的限制,有時您可能沒法得到足夠好的性能。在這些狀況下,最好的解決方案多是變動產品需求。
最後,你可能聽過css的will-change屬性。在特定的狀況下它能夠幫助你,可是使用很差也會有必定的風險。最好不要過分使用它。
當咱們調用任何 DOM 事件的時候,有一個去抖(debounce)或者節流(throttle)函數時頗有必要的。它可讓咱們把這個函數的調用次數減小到咱們想要的最低限度,以此來提升性能。
一般像這樣寫:window.addEventListener(‘resize’, _.throttle(callback)),可是爲何咱們不能把它也運用到 React Components callbacks 裏呢?
讓咱們看下面這個組件:
有沒有注意到,咱們每次輸入改變都會調用this.props.onChange, 它會被調用屢次,雖然不少調用都是非必需的。若是父級正在根據onChange回調進行 DOM 更改或者任何其餘比較繁重的操做,咱們的應用會變得很卡頓。
可能的解決辦法
其實咱們能夠這樣改進:
如今,只有在用戶輸入完成後才調用props.onChange, 這樣就阻止了不少沒必要要的事件操做。
另外類似的解決辦法還有函數節流(throtle).點擊查看throttle和debounce的區別。
這些工具應該能夠幫助您處理一些咱們在React應用程序中遇到的性能問題。經過明智地使用shouldComponentUpdate,控制你對DOM作的改變,並經過debounce / throttle來延遲迴調,你能夠大大地提升你的應用程序的性能。
若是你想測試開發遇到的狀況,請查看UiZoo。它是React組件的一個動態組件庫,它能夠解析你的組件並展現給你,讓你能夠開發,測試或與他人共享。
(本文所涉及的示例源碼 請到譯者github下載)