如何提高你的 React 應(yīng)用的性能

如何提高你的 React 應(yīng)用的性能

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

本文旨在總結(jié)常見的性能缺陷,以及如何來避免這些缺陷。

性能問題在web應(yīng)用開發(fā)中不是什么新鮮事。

我們每個(gè)人都有這樣的時(shí)刻,當(dāng)你把一個(gè)新的Component組件放到你的app中,你會(huì)突然發(fā)現(xiàn)你嘗試的每一個(gè)用戶交互動(dòng)作都與期望的效果有很明顯的滯后。有時(shí),你可以重復(fù)使用多個(gè)同樣的組件,這種尷尬的動(dòng)效滯后會(huì)更加明顯。像下面這樣:


在那一刻你也許心里已經(jīng)給寫這個(gè)組件的人起了好幾個(gè)綽號(hào)了。但是最好的辦法是:做些什么,是的,你可以的!

我們將重點(diǎn)解決以下幾個(gè)常見的 React 性能問題:

1.錯(cuò)誤的 shouldComponentUpdate 實(shí)現(xiàn) ,為什么 PureComponent 沒能拯救你。

2.太快的改變 DOM。

3.濫用事件(events)和 回調(diào)(callbacks)。

對(duì)于以上的每個(gè)問題,我們先解釋問題的根源,然后我們提出一些簡(jiǎn)單易用的方法來避免它。

管好你的 shouldComponentUpdate

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

React.Component 默認(rèn)的實(shí)現(xiàn) shouldComponentUpdate 是返回true.

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

為此,你會(huì)想到我們應(yīng)該在實(shí)現(xiàn) shouldComponentUpdate 的時(shí)候更謹(jǐn)慎些。

問題

讓我們看一個(gè)簡(jiǎn)單的使用 shouldComponentUpdate 的例子:


Simple shallow implementation: 'this.props.children?!== nextProps.children', but it's always returning true

code

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

不起作用是因?yàn)?React 每次渲染的時(shí)候創(chuàng)建了一個(gè)新的 ReactElement!

這就意味著 在 shouldComponentUpdate函數(shù)中 Shallow Comparision 如:return this.props.children?!== nextProps.children;幾乎就相當(dāng)于return true;

根據(jù)我的經(jīng)驗(yàn),大多數(shù)組件通常都以某種方式支持 ReactElement props(PropTypes.node or PropTyps.elemtn)比如像children這是很常見的情況。

那么,?PureComonent又是怎樣的呢?

React.PureComponent 是React.Component 的另外一種方式。它不是總在其 shouldComponent 實(shí)現(xiàn)中返回true,而是 props 和 state 的淺層比較。

使用 PureComponent 會(huì)返回同樣的結(jié)果,如下:


PureComponent component is still?always returning true


這是 PureComponent 特性的bug嗎?我不確定。我們需要知道的是,PureComponent 在大多數(shù)情況下不起作用,它并不能阻止一些不必要的更新。

可能的解決方案

我們第一點(diǎn)想到的是——進(jìn)行深度比較! 這確實(shí)管用,但是它有兩個(gè)重要缺陷:

1. 運(yùn)行深度比較本身是一個(gè)過程比較長(zhǎng),比較重,比較耗時(shí)的動(dòng)作。因此,在 shouldComponent 函數(shù)運(yùn)行結(jié)束之后,render 函數(shù)才能運(yùn)行。這樣一來性能非但不能提升反而會(huì)變得更差。

2. 這只是基于當(dāng)前的 React Elements 實(shí)現(xiàn),在未來版本中可能會(huì)取消。

綜上,在我看來,使用深度比較并不是一個(gè)好的解決辦法。

為了尋找到更好的解決方案,我研究了一些其他的虛擬 DOM 庫,看看他們是怎么解決這個(gè)問題的。

我發(fā)現(xiàn)了 Vue 作者Evan You 一個(gè)關(guān)于在Vue.js中添加 類React shouldeComponent 的 feature request 發(fā)表的一個(gè)有意思的評(píng)論。他解釋到,這個(gè)問題并不能通過 "diffing" 虛擬DOM解決,因?yàn)樗泻芏辔粗膯栴}。依賴 React Elements 來檢測(cè)組件中的狀態(tài)變化并不是一個(gè)可行的解決方案。

在實(shí)際應(yīng)用中,不應(yīng)該在 shouldComponentUpdate 的實(shí)現(xiàn)中使用 React Elements 的比較作為返回結(jié)果。相反,應(yīng)該使用某種狀態(tài)的改變來告訴組件是否應(yīng)該更新。

我們應(yīng)該基于prop的不同來通知 state 的改變,而不是通過使用this.props.children?!== nextProps.children。最好是一個(gè)數(shù)字或者字符串,這樣比較會(huì)更快。

我們甚至可以使用一個(gè)新的 prop 專門用來通知組件是否應(yīng)該更新。

更進(jìn)一步,我和我的同事創(chuàng)建了一個(gè)高階組件(HOC)。這個(gè)組件使用繼承反轉(zhuǎn)(Inheritance Inversion)來擴(kuò)展通用的 shouldComponent 實(shí)現(xiàn),也是 PureComponent 的替代方案。 而且確實(shí)有效。代碼在這里:

https://github.com/NoamELB/shouldComponentUpdate-Children

必須說明的是,這只是一個(gè)通用的實(shí)現(xiàn),所以并不是適用所有的情況。具體可以參考這里

例子在這里,使用了一個(gè)自定義的 shouldComponentUpdate 實(shí)現(xiàn)。正如上面提到的,它確實(shí)不會(huì)再進(jìn)行不必要的渲染了。


幾種比較:


具體示例代碼可以參考這里。

允許你的組件擴(kuò)張

你是否在你的應(yīng)用中多次使用相同的組件,致使你的應(yīng)用非常重動(dòng)畫也很卡頓,有時(shí)候即使使用一個(gè)也會(huì)導(dǎo)致應(yīng)用性能的損耗?

問題

在創(chuàng)建復(fù)雜的組件時(shí),你可能需要執(zhí)行一些自定義 DOM 的操作。在創(chuàng)建的時(shí)候你可能會(huì)遇到兩個(gè)問題:

1. 觸發(fā)太多布局(Layout)而沒有使用觸發(fā)復(fù)合(Composite)或者重繪(Paint)

2. 太多沒必要的Layout.多次讀寫DOM,導(dǎo)致 DOM不必要的重新計(jì)算。

讓我們看下 原生 Collapse 組件,在0和內(nèi)容高度之間改變它的高度。點(diǎn)擊查看


當(dāng)使用一個(gè)這樣的組件時(shí),可以正常展示。但是當(dāng)你多次使用的時(shí)候......


點(diǎn)擊查看具體效果

如果你不是在移動(dòng)設(shè)備上查看,可能感覺不明顯。需要將你的chrome performance選項(xiàng)調(diào)到 6x slowdown


可能的解決方案

讓我們分析下 Collapse 組件發(fā)生了什么——這是高度改變的時(shí)候的代碼:


這里有兩個(gè)問題需要注意:

1. 我們改變的height屬性,根據(jù)csstriggers.com這個(gè)列表,改變高度(height)觸發(fā)了布局(Layout)的重新計(jì)算。如果我們?cè)O(shè)法改變類似transform的東西,那只會(huì)觸發(fā)Composite,并且會(huì)更平滑些,對(duì)嗎?

事實(shí)正式如此,這樣會(huì)表現(xiàn)更好,但是這樣就會(huì)在Collapse組件下留下一個(gè)空白,因?yàn)槲覀儧]有改變它的高度。

2. 上面代碼的第三行,這是常見的改變高度出發(fā)Layout的濫用:我們從DOM讀取了高度this.contentEl.scrollHeight然后又通過this.containerEl.style.height對(duì)DOM設(shè)置了高度,然后多次重復(fù)這樣的操作。

如果我們可以成組的一次性讀取過來高度,然后再一次性設(shè)置高度,這樣不是更好嗎?

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


requestAnimationFrame能保證你的代碼在瀏覽器下一幀觸發(fā),減少頁面繪制成本,按需批量繪制。讓你的動(dòng)畫更流暢。點(diǎn)擊查看具體實(shí)現(xiàn)

這樣用起來可能比較麻煩,那么可以使用內(nèi)置組件或者使用第三方庫比如Fastdom, Fastdom也是基于requesAnimationFrame 的原理通過批量處理DOM 讀取/寫入 操作來消除頻繁的Layout操作。

值得一提的是,由于瀏覽器和設(shè)備功能的限制,有時(shí)您可能無法獲得足夠好的性能。在這些情況下,最好的解決方案可能是變更產(chǎn)品需求。

最后,你可能聽過css的will-change屬性。在特定的情況下它可以幫助你,但是使用不好也會(huì)有一定的風(fēng)險(xiǎn)。最好不要過度使用它。

管住你的 callbacks

當(dāng)我們調(diào)用任何 DOM 事件的時(shí)候,有一個(gè)去抖(debounce)或者節(jié)流(throttle)函數(shù)時(shí)很有必要的。它可以讓我們把這個(gè)函數(shù)的調(diào)用次數(shù)減少到我們想要的最低限度,以此來提高性能。

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

問題

讓我們看下面這個(gè)組件:


有沒有注意到,我們每次輸入改變都會(huì)調(diào)用this.props.onChange, 它會(huì)被調(diào)用多次,雖然很多調(diào)用都是非必需的。如果父級(jí)正在根據(jù)onChange回調(diào)進(jìn)行 DOM 更改或者任何其他比較繁重的操作,我們的應(yīng)用會(huì)變得很卡頓。

可能的解決辦法

其實(shí)我們可以這樣改進(jìn):


Debounce the event

現(xiàn)在,只有在用戶輸入完成后才調(diào)用props.onChange, 這樣就阻止了很多不必要的事件操作。

另外相似的解決辦法還有函數(shù)節(jié)流(throtle).點(diǎn)擊查看throttledebounce區(qū)別。

總結(jié)

這些工具應(yīng)該可以幫助您處理一些我們?cè)赗eact應(yīng)用程序中遇到的性能問題。通過明智地使用shouldComponentUpdate,控制你對(duì)DOM做的改變,并通過debounce / throttle來延遲回調(diào),你可以大大地提高你的應(yīng)用程序的性能。

如果你想測(cè)試開發(fā)遇到的情況,請(qǐng)查看UiZoo。它是React組件的一個(gè)動(dòng)態(tài)組件庫,它可以解析你的組件并展示給你,讓你可以開發(fā),測(cè)試或與他人共享。

完整示例代碼見:示例代碼



最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容