如何提高你的 React 應用的效能

趙趙發表於2019-03-04

How to greatly improve your React app performance)- Noam Elboim / from medium

本文旨在總結常見的效能缺陷,以及如何來避免這些缺陷。

(本文所涉及的示例原始碼 請到譯者github下載)

效能問題在web應用開發中不是什麼新鮮事。

我們每個人都有這樣的時刻,當你把一個新的Component元件放到你的app中,你會突然發現你嘗試的每一個使用者互動動作都與期望的效果有很明顯的滯後。有時,你可以重複使用多個同樣的元件,這種尷尬的動效滯後會更加明顯。像下面這樣:


如何提高你的 React 應用的效能

在那一刻你也許心裡已經給寫這個元件的人起了好幾個綽號了。但是最好的辦法是:做些什麼,是的,你可以的!

我們將重點解決以下幾個常見的 React 效能問題:

1.錯誤的 shouldComponentUpdate 實現 ,為什麼 PureComponent 沒能拯救你。

2.太快的改變 DOM。

3.濫用事件(events)和 回撥(callbacks)。

對於以上的每個問題,我們先解釋問題的根源,然後我們提出一些簡單易用的方法來避免它。

管好你的 shouldComponentUpdate

元件的component 鉤子函式 shouldComponentUpdate 的本意是用來阻止一些非必需的渲染(render), shouldComponentUpdate將即將更新的props和state作為引數,如果返回值是true, render函式就執行,否則不執行 render.

React.Component 預設的實現 shouldComponentUpdate 是返回true.

越多的render渲染意味著耗費越多的時間。所以我們需要防止不必要的更新來減少額外的時間。

為此,你會想到我們應該在實現 shouldComponentUpdate 的時候更謹慎些。

問題

讓我們看一個簡單的使用 shouldComponentUpdate 的例子:


如何提高你的 React 應用的效能
Simple shallow implementation: 'this.props.children !== nextProps.children', but it's always returning true

code

等下,為什麼不起作用呢?

不起作用是因為 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 會返回同樣的結果,如下:


如何提高你的 React 應用的效能
PureComponent component is still always returning true


這是 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 的替代方案。 而且確實有效。程式碼在這裡:

github.com/NoamELB/sho…

必須說明的是,這只是一個通用的實現,所以並不是適用所有的情況。具體可以參考這裡

例子在這裡,使用了一個自定義的 shouldComponentUpdate 實現。正如上面提到的,它確實不會再進行不必要的渲染了。


如何提高你的 React 應用的效能

幾種比較:


如何提高你的 React 應用的效能

具體示例程式碼可以參考這裡

允許你的元件擴張

你是否在你的應用中多次使用相同的元件,致使你的應用非常重動畫也很卡頓,有時候即使使用一個也會導致應用效能的損耗?

問題

在建立複雜的元件時,你可能需要執行一些自定義 DOM 的操作。在建立的時候你可能會遇到兩個問題:

1. 觸發太多佈局(Layout)而沒有使用觸發複合(Composite)或者重繪(Paint)

2. 太多沒必要的Layout.多次讀寫DOM,導致 DOM不必要的重新計算。

讓我們看下 原生 Collapse 元件,在0和內容高度之間改變它的高度。點選檢視


如何提高你的 React 應用的效能

當使用一個這樣的元件時,可以正常展示。但是當你多次使用的時候......


如何提高你的 React 應用的效能

點選檢視具體效果

如果你不是在移動裝置上檢視,可能感覺不明顯。需要將你的chrome performance選項調到 6x slowdown


如何提高你的 React 應用的效能

可能的解決方案

讓我們分析下 Collapse 元件發生了什麼——這是高度改變的時候的程式碼:


如何提高你的 React 應用的效能

這裡有兩個問題需要注意:

1. 我們改變的height屬性,根據csstriggers.com這個列表,改變高度(height)觸發了佈局(Layout)的重新計算。如果我們設法改變類似transform的東西,那隻會觸發Composite,並且會更平滑些,對嗎?

事實正式如此,這樣會表現更好,但是這樣就會在Collapse元件下留下一個空白,因為我們沒有改變它的高度。

2. 上面程式碼的第三行,這是常見的改變高度出發Layout的濫用:我們從DOM讀取了高度this.contentEl.scrollHeight然後又通過this.containerEl.style.height對DOM設定了高度,然後多次重複這樣的操作。

如果我們可以成組的一次性讀取過來高度,然後再一次性設定高度,這樣不是更好嗎?

批量的讀寫 DOM 是一個很好的減少 Layout 的嘗試。我們可以使用requestAnimationFrame對DOM 讀寫進行批量處理,像下面這樣:


如何提高你的 React 應用的效能

requestAnimationFrame能保證你的程式碼在瀏覽器下一幀觸發,減少頁面繪製成本,按需批量繪製。讓你的動畫更流暢。點選檢視具體實現

這樣用起來可能比較麻煩,那麼可以使用內建元件或者使用第三方庫比如Fastdom, Fastdom也是基於requesAnimationFrame 的原理通過批量處理DOM 讀取/寫入 操作來消除頻繁的Layout操作。

值得一提的是,由於瀏覽器和裝置功能的限制,有時您可能無法獲得足夠好的效能。在這些情況下,最好的解決方案可能是變更產品需求。

最後,你可能聽過css的will-change屬性。在特定的情況下它可以幫助你,但是使用不好也會有一定的風險。最好不要過度使用它。

管住你的 callbacks

當我們呼叫任何 DOM 事件的時候,有一個去抖(debounce)或者節流(throttle)函式時很有必要的。它可以讓我們把這個函式的呼叫次數減少到我們想要的最低限度,以此來提高效能。

通常像這樣寫:window.addEventListener(‘resize’, _.throttle(callback)),但是為什麼我們不能把它也運用到 React Components callbacks 裡呢?

問題

讓我們看下面這個元件:


如何提高你的 React 應用的效能

有沒有注意到,我們每次輸入改變都會呼叫this.props.onChange, 它會被呼叫多次,雖然很多呼叫都是非必需的。如果父級正在根據onChange回撥進行 DOM 更改或者任何其他比較繁重的操作,我們的應用會變得很卡頓。

可能的解決辦法

其實我們可以這樣改進:


如何提高你的 React 應用的效能
Debounce the event

現在,只有在使用者輸入完成後才呼叫props.onChange, 這樣就阻止了很多不必要的事件操作。

另外相似的解決辦法還有函式節流(throtle).點選檢視throttledebounce區別

總結

這些工具應該可以幫助您處理一些我們在React應用程式中遇到的效能問題。通過明智地使用shouldComponentUpdate,控制你對DOM做的改變,並通過debounce / throttle來延遲迴調,你可以大大地提高你的應用程式的效能。

如果你想測試開發遇到的情況,請檢視UiZoo。它是React元件的一個動態元件庫,它可以解析你的元件並展示給你,讓你可以開發,測試或與他人共享。

(本文所涉及的示例原始碼 請到譯者github下載)




相關文章