前言
用 React 一段時(shí)間了,也做了不少列表頁浪箭。在用 React 做無限下拉加載的列表頁時(shí)發(fā)現(xiàn)個(gè)問題:頁面前幾頁渲染速度還挺快的灿渴,但是越往下拉加載內(nèi)容頁面的渲染就越慢。這是怎么回事呢巩搏?
讓我們先來看下 React 的組件渲染流程吧。
React 的組件渲染流程
React 的組件渲染分為初始化渲染和更新渲染趾代。在初始化時(shí)贯底,React 會(huì)調(diào)用根組件下所有組件的 render 方法進(jìn)行渲染。
在每個(gè)生命周期更新時(shí)撒强,React 會(huì)先調(diào)用 shouldComponentUpdate(nextProps, nextState) 方法來判斷該組件是否需要更新禽捆。該方法會(huì)返回 true 或 false 來表示更新或不需要更新。如果不需要更新飘哨,則直接保持不變胚想;如果需要更新,則調(diào)用 render 方法生成新的虛擬 DOM芽隆,然后再用 diff 算法與舊的虛擬 DOM 進(jìn)行對比浊服,如果結(jié)果一致就不更新;如果對比不同胚吁,則根據(jù)最小粒度改變?nèi)ジ?DOM臼闻。
整個(gè)過程如下圖所示。
ShouldComponentUpdate 在默認(rèn)情況下返回的是 true囤采。也就是說 React 默認(rèn)會(huì)調(diào)用所有組件的 render
方法生成虛擬 DOM述呐,然后再與舊虛擬 DOM 比較以確定最終組件是否需要更新。這個(gè) render 和 diff
對比的過程對于只是兄弟組件發(fā)生了改變蕉毯,而本身并沒有變化的組件來說乓搬,很明顯存在資源浪費(fèi)思犁。
那么如何能直觀的知道這些浪費(fèi)都發(fā)生在哪些過程中呢?這就輪到 Perf 出場了进肯。
接下來讓我們先來了解下什么是 Perf激蹲,再看看它都能做些什么。
什么是 Perf 江掩,它能做些什么?
Perf是 react 官方提供的性能分析工具学辱,可以對我們的應(yīng)用進(jìn)行整體性能分析并提供性能數(shù)據(jù)。
直接來看下具體有哪些 API 吧:
Perf.start():開始測量环形。
Perf.stop():停止測量策泣。
Perf.getLastMeasurements():在停止測量之后調(diào)用,用來獲取 measurements抬吟。
接下來就可以打印出性能數(shù)據(jù)了:
Perf.printInclusive(measurements):打印出所花費(fèi)的整體時(shí)間萨咕。
Perf.printExclusive(measurements):打印出處理 props、getInitialState火本、調(diào)用 componentWillMount 和 componentDidMount 等的時(shí)間危队,這里面不包含 mount 組件的時(shí)間。
Perf.printWasted(measurements):打印出測量時(shí)段內(nèi)所浪費(fèi)的時(shí)間钙畔。這部分信息是分析數(shù)據(jù)中最有用的一部分了茫陆。我們可以通過這個(gè)數(shù)據(jù)找出時(shí)間被浪費(fèi)在了哪兒。浪費(fèi)一般出現(xiàn)在組件沒有渲染任何東西的時(shí)候擎析,如上文中提到的簿盅,組件在
render 出新的虛擬 DOM 和舊的虛擬 DOM 對比之后,發(fā)現(xiàn)不需要更新組件叔锐。最理想的情況這個(gè)的返回值是一個(gè)空數(shù)組。
Perf.printOperations(measurements):打印出分析時(shí)段內(nèi)發(fā)生的底層 DOM 操作见秽。
目前不少 React 性能優(yōu)化的文檔里都有提到可以通過 shouldComponenentUpdate 和 Perf 來進(jìn)行優(yōu)化愉烙,但是卻沒有進(jìn)行詳細(xì)的說明。一開始的時(shí)候我是困惑的:
Perf 是怎么跑起來的解取?
在什么時(shí)候執(zhí)行比較好步责?
性能報(bào)表中的各個(gè)指標(biāo)是什么意思呢?
怎么結(jié)合這些數(shù)據(jù)來進(jìn)行優(yōu)化禀苦?
翻了不少文檔并實(shí)踐之后蔓肯,以我們到家業(yè)務(wù)的店鋪列表組件的優(yōu)化為例,總結(jié)出來了以下的使用步驟振乏,僅供大家參考蔗包。
Perf 怎么用?
使用步驟:
步驟一:獲取
先在頁面把原來的 react.js 替換成帶組件的版本 react-with-addons.js
這里要補(bǔ)充說明下關(guān)于使用的 react-with-addons 的版本
推薦使用最新版本 15.3.2。
如果使用 15.1.0 版本慧邮,react-with-addons 有可能會(huì)出現(xiàn) Warning: There is an
internal error in the React performance measurement code. We did not
expect componentWillMount timer to stop while no timer is still in
progress for another instance. Please report this as a bug in
React调限。另外會(huì)出現(xiàn)有時(shí)候執(zhí)行 React.addons.Perf.printOperations(measurements);
打印不出信息來等一些奇怪的問題舟陆。
Perf 是在0.11.0 版本中新增的, 然后在 [15.1.0 版本]中進(jìn)行了重構(gòu)耻矮,并在后續(xù)版本中修復(fù)了不少 bug秦躯,目前還在逐漸完善的過程中。另外要注意的是:在非生產(chǎn)環(huán)境是不能使用 Perf 的裆装。
步驟二:調(diào)用
方式一:直接在瀏覽器里調(diào)用
在瀏覽器的控制臺(tái)里輸入:
React.addons.Perf.start();
執(zhí)行某個(gè)操作踱承,如滾動(dòng)屏幕來加載列表
然后在控制臺(tái)里輸入如下代碼(以 printWasted 為例):
React.addons.Perf.stop();varmeasurements = React.addons.Perf.getLastMeasurements(); React.addons.Perf.printWasted(measurements);
這樣就能夠看到打印出來這一過程所浪費(fèi)的時(shí)間了。
方式二:添加到組件代碼中
在組件的 componentDidUpdate 方法中調(diào)用哨免,這樣可以在組件每次發(fā)生更新時(shí)打印出各個(gè)性能數(shù)據(jù)茎活。
componentWillMount() {? React.addons.Perf.start();// Your code}componentDidUpdate() {// Your code.letPerf = React.addons.Perf;? Perf.stop();letmeasurements = React.addons.Perf.getLastMeasurements();if(measurements.length >0) {? ? Perf.printInclusive(measurements);? ? Perf.printExclusive(measurements);? ? Perf.printWasted(measurements);? ? Perf.printOperations(measurements);? ? Perf.start();// clears measurements and try it again}}
這樣就可以在頁面連續(xù)滾動(dòng)時(shí)打印出多個(gè)數(shù)據(jù)。
接下來讓我們看下在這些數(shù)據(jù)中可以發(fā)現(xiàn)什么铁瞒。
數(shù)據(jù)指標(biāo)分析
店鋪列表在每次下拉刷新時(shí)妙色,先變更列表加載狀態(tài),再渲染出列表內(nèi)容慧耍。以從第11頁下拉翻到第12頁為例身辨,我們先來看下優(yōu)化前后的效果對比圖,如下:
優(yōu)化前:
Perf.printInclusive(measurements)
Perf.printExclusive(measurements);
Perf.printWasted(measurements)
優(yōu)化后:
Perf.printInclusive(measurements)
Perf.printExclusive(measurements);
Perf.printWasted(measurements)
從上述圖表中可以看到芍碧,優(yōu)化之后整體的渲染時(shí)間較時(shí)間有較大減少煌珊,且浪費(fèi)時(shí)間的時(shí)間也大幅減少,在執(zhí)行過程中泌豆,有個(gè)生命周期中的浪費(fèi)時(shí)間已經(jīng)減為0了定庵。
下面就來看看這個(gè)優(yōu)化是怎么做的吧。
優(yōu)化方案
拆分組件踪危,結(jié)合 shouldComponentUpdate蔬浙,以減少重繪次數(shù)。
對于靜態(tài)組件贞远,shouldComponentUpdate 返回 false畴博;
對于組件存在變化的情況
如果變化的 props 或 state 不多,且層次不深蓝仲,則可以在 shouldComponentUpdate(nextProps,
nextState) 里比較新老 props 和 state俱病,在目標(biāo) props 或 state 發(fā)生變化時(shí) return ture,其余情況都
return false袱结。
如果變化的 props 和 state 多亮隙,或者層次深,則最好把組件拆分成變化的和不變化的部分垢夹。
注意:這里必須要先確保組件是靜態(tài)的溢吻,即在 componentDidMount 后不會(huì)有任何變化,否則不能直接 return false果元。
在店鋪列表組件優(yōu)化的過程中煤裙,一開始沒有留意到 ShopCard 組件中的優(yōu)惠區(qū)域高度是會(huì)根據(jù)優(yōu)惠條數(shù)的不同而有所不同的掩完,并且具有收起和展開的功能,直接 return false 后導(dǎo)致這個(gè)區(qū)塊撐開的高度有問題了硼砰,并且收起/展開的功能也失效了且蓬。
改出問題的樣子:
正常情況初始時(shí)的樣子:
正常情況展開后的樣子:
就拿 ShopCard 組件的代碼作為例子看下 shouldComponentUpdate 是怎么樣的吧:
shouldComponentUpdate(nextProps, nextState) {let{ shouldShowMoreActivities, height } =this.state;returnshouldShowMoreActivities && height !== nextState.height;? }
因?yàn)檫@個(gè)組件只會(huì)受是否有優(yōu)惠活動(dòng)和優(yōu)惠撐開后的高度所影響,所以只要關(guān)注 shouldShowMoreActivities 和 height 這兩個(gè) state 即可题翰。
修改后整體效果如下:
Perf.printInclusive(measurements)
Perf.printExclusive(measurements);
Perf.printWasted(measurements)
從優(yōu)化后的效果圖中可以看到 ShopCard 組件只渲染了最后一頁增加的7項(xiàng)恶阴,另外,render time豹障、render count 都從原來的上百減至幾個(gè)了冯事,且浪費(fèi)的時(shí)間也從原來的幾十毫秒減為個(gè)位數(shù)了。效果還是比較明顯的血公。
但是如果每個(gè)組件都要手動(dòng)覆蓋 shouldComponentUpdate 方法也是比較費(fèi)時(shí)的事情昵仅,并且這個(gè)方法的重寫也需要謹(jǐn)慎,可能會(huì)帶來意想不到的問題累魔。
接下來讓我們看下 React 有沒有為這個(gè)事情做點(diǎn)什么吧摔笤。
PureRenderMixin
如果你的組件在相同輸入的時(shí)候都能夠有相同的產(chǎn)出,那么就可以使用 React 提供的 PureRenderMixin
插件垦写,它會(huì)自行為組件綁定 shouldComponentUpdate 方法吕世,對現(xiàn)有的子組件的 state 和 props
進(jìn)行判斷。但是它只支持基本類型的淺度比較梯投,如果組件的 props 和 state 數(shù)據(jù)結(jié)構(gòu)層次復(fù)雜則不適用命辖。使用方法如下:
classShopextendsReact.Component{constructor(props) {super(props);this.shouldComponentUpdate = React.addons.PureRenderMixin.shouldComponentUpdate.bind(this);? }? render() {// Your code}}
說明:如果頁面上引入的是 react.js,可以自行安裝 react-addons-pure-render-mixin 依賴后以如下方式引入:
importPureRenderMixinfrom'react-addons-pure-render-mixin';
效果如下圖(以 Perf.printWasted(measurements) 為例):
相對于最初版本的已經(jīng)少了很多分蓖,不過比自己實(shí)現(xiàn) shouldComponentUpdate 還是多浪費(fèi)了 ShopCard 的 15 次 render尔艇。
React.PureComponent
在 react 的最新版本里面,還提供了 React.PureComponent 的基礎(chǔ)類么鹤,直接把原來的 React.Component 替換成 React.PureComponent 即可终娃。
效果如下圖(以 Perf.printWasted(measurements)
為例):
效果和使用 PureRenderMixin 差不多。只是需要注意的是 PureComponent 是在15.3.0版本中才開始支持的午磁。
另外尝抖,F(xiàn)acebook 還提供了一個(gè)專門處理不可變數(shù)據(jù)的庫immutable.js毡们,大家感興趣的可自行了解迅皇。
清理組件之間不關(guān)聯(lián)的 props 映射
當(dāng)父組件包含多個(gè)子組件,子組件之間存在交互的情況下衙熔,有些場景里父組件只是受子組件的某一個(gè)屬性影響登颓,或者一個(gè)子組件只受另外子組件的某些屬性影響,那么在 mapStateToProps 的時(shí)候就要在各自的 Container 里面把受影響組件的那幾個(gè)相關(guān) state 映射到 props 里红氯。
但是組件一多框咙,屬性一多咕痛,這就是件很費(fèi)神的事情,尤其是寫的過程中發(fā)現(xiàn)要增加 state 了喇嘱,就要在關(guān)聯(lián)組件的 mapStateToProps 中挨個(gè)加一遍茉贡,有時(shí)候發(fā)現(xiàn)某個(gè)屬性用不到了又要挨個(gè)刪一遍。不知道大家有沒有這種體驗(yàn)者铜,還是我的使用姿勢不對腔丧?反正每當(dāng)這種時(shí)候我就特別想把要用到的狀態(tài)所屬的組件定義的整個(gè) state 對象塞到自己的 props 里,這樣不管后面加多少 state作烟,也不用再加一遍愉粤,而是直接拿這個(gè)對象的屬性就好了。
但是拿撩,這樣會(huì)有一個(gè)副作用衣厘,某組件只要一個(gè)屬性更新了,映射了該組件所屬 state 到自己的 props 里的組件就會(huì)觸發(fā)重新渲染了压恒。而如上所說影暴,shouldComponentUpdate 和 PureComponent 適用場景有限。因此涎显,在代碼層面能做的優(yōu)化還是直接做掉吧坤检,而且梳理一遍 props 和 state,可以對組件之間的交互邏輯更了解期吓。
在簡化了 props 后早歇,自己編寫 shouldComponentUpdate 也會(huì)簡單很多。
效果如下圖(以 Perf.printWasted(measurements) 為例):
從結(jié)果中可以看到少了很多浪費(fèi)時(shí)間的項(xiàng)目讨勤。
React 頁面的性能優(yōu)化方案還有很多箭跳,如合并 setState,合并 dispatch潭千,漸進(jìn)式渲染等谱姓,key,這里就先不一一展開了刨晴,后續(xù)再講屉来。
小結(jié)
本文主要講述了如何使用 Perf 性能分析工具結(jié)合 React 提供的 shouldComponentUpdate 方法、PureRenderMixin 插件 和 PureComponent 組件來提高 React 組件的渲染性能狈癞。
還有其他很多工具如 Chrome 的 Timeline 和 Profiles 也能夠幫助我們發(fā)現(xiàn)代碼中的問題茄靠。工具在很大程度上能夠給我們帶來效率上提升。
但在使用工具的同時(shí)蝶桶,我們也要提高自己代碼的質(zhì)量慨绳,合理添加注釋,及時(shí)清理垃圾代碼,優(yōu)化代碼脐雪,這樣不管是代碼執(zhí)行效率厌小,還是后續(xù)的維護(hù)都能更高效。