<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          歪門邪道性能優(yōu)化:魔改三方庫源碼,性能提高幾十倍!

          共 19305字,需瀏覽 39分鐘

           ·

          2021-02-01 23:41

          本文會(huì)分享一個(gè)React性能優(yōu)化的故事,這也是我在工作中真實(shí)遇到的故事,最終我們是通過魔改第三方庫源碼將它性能提高了幾十倍。這個(gè)第三方庫也是很有名的,在GitHub上有4.5k star,這就是:react-big-calendar[1]

          這個(gè)工作不是我一個(gè)人做的,而是我們團(tuán)隊(duì)幾個(gè)月前共同完成的,我覺得挺有意思,就將它復(fù)盤總結(jié)了一下,分享給大家

          在本文中你可以看到:

          1.React常用性能分析工具的使用介紹2.性能問題的定位思路3.常見性能優(yōu)化的方式和效果:PureComponent,?shouldComponentUpdate,?Context,?按需渲染等等4.對(duì)于第三方庫的問題的解決思路

          關(guān)于我工作中遇到的故事,我前面其實(shí)也分享過兩篇文章了:

          1.

          速度提高幾百倍,記一次數(shù)據(jù)結(jié)構(gòu)在實(shí)際工作中的運(yùn)用[2]

          2.

          使用mono-repo實(shí)現(xiàn)跨項(xiàng)目組件共享[3]



          特別是速度提高幾百倍,記一次數(shù)據(jù)結(jié)構(gòu)在實(shí)際工作中的運(yùn)用,這篇文章在某平臺(tái)單篇閱讀都有三萬多,有些朋友也提出了質(zhì)疑。覺得我這篇文章里面提到的問題現(xiàn)實(shí)中不太可能遇到,里面的性能優(yōu)化更多是偏理論的,有點(diǎn)杞人憂天。這個(gè)觀點(diǎn)我基本是認(rèn)可的,我在那篇文章正文也提到過可能是個(gè)偽需求,但是技術(shù)問題本來很多就是理論上的,我們?cè)趌eetcode上刷題還是純理論呢,理論結(jié)合實(shí)際才能發(fā)揮其真正的價(jià)值,即使是杞人憂天,但是性能確實(shí)快上了那么一點(diǎn)點(diǎn),也給大家提供了另一個(gè)思路,我覺得也是值得的。

          與之相對(duì)的,本文提到的問題完全不是杞人憂天了,而是實(shí)打?qū)嵉挠脩粜枨螅覀兘?jīng)過用戶調(diào)研,發(fā)現(xiàn)用戶確實(shí)有這么多數(shù)據(jù)量,需求上不可能再壓縮了,只能技術(shù)上優(yōu)化,這也是逼得我們?nèi)ジ牡谌綆煸创a的原因。

          需求背景

          老規(guī)矩,為了讓大家快速理解我們遇到的問題,我會(huì)簡(jiǎn)單講一下我們的需求背景。我還是在那家外企,不久前我們接到一個(gè)需求:做一個(gè)體育場(chǎng)館管理Web App。這里面有一個(gè)核心功能是場(chǎng)館日程的管理,有點(diǎn)類似于大家Outlook里面的Calendar。大家如果用過Outlook,應(yīng)該對(duì)他的Calendar有印象,基本上我們的會(huì)議及其他日程安排都可以很方便的放在里面。我們要做的這個(gè)也是類似的,體育場(chǎng)館的老板可以用這個(gè)日歷來管理他下面場(chǎng)地的預(yù)定。

          假設(shè)你現(xiàn)在是一個(gè)羽毛球場(chǎng)的老板,來了個(gè)客戶說,嘿,老板,這周六場(chǎng)地有空嗎,我訂一個(gè)小時(shí)呢!場(chǎng)館每天都很多預(yù)定,你也不記得周六有沒有空,所以你打開我們的網(wǎng)站,看了下日歷:

          你發(fā)現(xiàn)1月15號(hào),也就是星期五有兩個(gè)預(yù)定,周六還全是空閑的,于是給他說:你運(yùn)氣真好,周六目前還沒人預(yù)定,時(shí)段隨便挑!上面這個(gè)截圖是react-big-calendar的官方示例,我們也是選定用他來搭建我們自己的應(yīng)用。

          真實(shí)場(chǎng)景

          上面這個(gè)例子只是說明下我們的應(yīng)用場(chǎng)景,里面預(yù)定只有兩個(gè),場(chǎng)地只有一塊。但是我們真實(shí)的客戶可比這個(gè)大多了,根據(jù)我們的調(diào)研,我們較大的客戶有數(shù)百塊場(chǎng)地,每個(gè)場(chǎng)地每天的預(yù)定可能有二三十個(gè)。上面那個(gè)例子我們換個(gè)生意比較好的老板,假設(shè)這個(gè)老板有20塊羽毛球場(chǎng)地,每天客戶都很多,某天還是來了個(gè)客戶說,嘿,老板,這周六場(chǎng)地有空嗎,我訂一個(gè)小時(shí)呢!但是這個(gè)老板生意很好,他看到的日歷是這樣的:

          本周場(chǎng)館1全滿!!如果老板想要為客戶找到一個(gè)有空的場(chǎng)地,他需要連續(xù)切換場(chǎng)館1,場(chǎng)館2。。。一直到場(chǎng)館20,手都點(diǎn)酸了。。。為了減少老板手的負(fù)擔(dān),我們的產(chǎn)品經(jīng)理提出一個(gè)需求,同時(shí)在頁面上顯示10個(gè)場(chǎng)館的日歷,好在react-big-calendar本身就是支持這個(gè)的,他把這個(gè)叫做resources[4]

          性能爆炸

          看起來我們要的基本功能react-big-calendar都能提供,前途還是很美好的,直到我們將真實(shí)的數(shù)據(jù)渲染到頁面上。。。我們的預(yù)定不僅僅是展示,還需要支持一系列的操作,比如編輯,復(fù)制,剪切,粘貼,拖拽等等。當(dāng)然這一切操作的前提都是選中這個(gè)預(yù)定,下面這個(gè)截圖是我選中某個(gè)預(yù)定的耗時(shí):

          僅僅是一個(gè)最簡(jiǎn)單的點(diǎn)擊事件,腳本執(zhí)行耗時(shí)6827ms,渲染耗時(shí)708ms,總計(jì)耗時(shí)7.5s左右,這TM!這玩意兒還想賣錢?送給我,我都不想用

          可能有朋友不知道這個(gè)性能怎么看,這其實(shí)是Chrome自帶的性能工具,基本步驟是:

          1.打開Chrome調(diào)試工具,點(diǎn)到Performance一欄2.點(diǎn)擊左上角的小圓點(diǎn),開始錄制3.執(zhí)行你想要的操作,我這里就是點(diǎn)擊一個(gè)預(yù)定4.等你想要的結(jié)果出來,我這里就是點(diǎn)擊的預(yù)定顏色加深5.再點(diǎn)擊左上角的小圓點(diǎn),結(jié)束錄制就可以看到了

          為了讓大家看得更清楚,我這里錄制了一個(gè)操作的動(dòng)圖,這個(gè)圖可以看到,點(diǎn)擊操作的響應(yīng)花了很長(zhǎng)時(shí)間,Chrome加載這個(gè)性能數(shù)據(jù)也花了很長(zhǎng)時(shí)間:

          【動(dòng)圖太大,微信公眾號(hào)不讓發(fā),可以點(diǎn)擊“閱讀原文”去掘金看。】

          測(cè)試數(shù)據(jù)量

          上面僅僅一個(gè)點(diǎn)擊耗時(shí)就七八秒,是因?yàn)槲夜室庥昧撕艽髷?shù)據(jù)量嗎?不是!我的測(cè)試數(shù)據(jù)量是完全按照用戶真實(shí)場(chǎng)景計(jì)算的:同時(shí)顯示10個(gè)場(chǎng)館,每個(gè)場(chǎng)館每天20個(gè)預(yù)定,上面使用的是周視圖,也就是可以同時(shí)看到7天的數(shù)據(jù),那總共顯示的預(yù)定就是:

          10 * 20 * 7 = 1400,總共1400個(gè)預(yù)定顯示在頁面上。

          為了跟上面這個(gè)龜速點(diǎn)擊做個(gè)對(duì)比,我再放下優(yōu)化后的動(dòng)圖,讓大家對(duì)后面這個(gè)長(zhǎng)篇大論實(shí)現(xiàn)的效果先有個(gè)預(yù)期:

          【動(dòng)圖太大,微信公眾號(hào)不讓發(fā),可以點(diǎn)擊“閱讀原文”去掘金看。

          定位問題

          我們一般印象中,React不至于這么慢啊,如果慢了,大概率是寫代碼的人沒寫好!我們都知道React有個(gè)虛擬樹,當(dāng)一個(gè)狀態(tài)改變了,我們只需要更新與這個(gè)狀態(tài)相關(guān)的節(jié)點(diǎn)就行了,出現(xiàn)這種情況,是不是他干了其他不必要的更新與渲染呢?為了解決這個(gè)疑惑,我們安裝了React專用調(diào)試工具:React Developer Tools[5]。這是一個(gè)Chrome的插件,Chrome插件市場(chǎng)可以下載,安裝成功后,Chrome的調(diào)試工具下面會(huì)多兩個(gè)Tab頁:

          Components這個(gè)Tab下有個(gè)設(shè)置,打開這個(gè)設(shè)置可以看到你每次操作觸發(fā)哪些組件更新,我們就是從這里面發(fā)現(xiàn)了一點(diǎn)驚喜:

          為了看清楚點(diǎn)擊事件觸發(fā)哪些更新,我們先減少數(shù)據(jù)量,只保留一兩個(gè)預(yù)定,然后打開這個(gè)設(shè)置看看:

          哼,這有點(diǎn)意思。。。我只是點(diǎn)擊一個(gè)預(yù)定,你把整個(gè)日歷的所有組件都給我更新了!那整個(gè)日歷有多少組件呢?上面這個(gè)圖可以看出10:00 AM10:30 AM之間是一個(gè)大格子,其實(shí)這個(gè)大格子中間還有條分割線,只是顏色較淡,看的不明顯,也就是說每15分鐘就是一個(gè)格子。這個(gè)15分鐘是可以配置的,你也可以設(shè)置為1分鐘,但是那樣格子更多,性能更差!我們是根據(jù)需求給用戶提供了15分鐘,30分鐘,1小時(shí)等三個(gè)選項(xiàng)。當(dāng)用戶選擇15分鐘的時(shí)候,渲染的格子最多,性能最差。

          那如果一個(gè)格子是15分鐘,總共有多少格子呢?一天是24 * 60 = 1440分鐘,15分鐘一個(gè)格子,總共96個(gè)格子。我們周視圖最多展示7天,那就是7 * 96 = 672格子,最多可以展示10個(gè)場(chǎng)館,就是672 * 10 = 6720個(gè)格子,這還沒算日期和時(shí)間本身占據(jù)的組件,四舍五入一下姑且就算7000個(gè)格子吧。

          我僅僅是點(diǎn)擊一下預(yù)定,你就把作為背景的7000個(gè)格子全部給我更新一遍,怪不得性能差

          再仔細(xì)看下上面這個(gè)動(dòng)圖,我點(diǎn)擊的是小的那個(gè)事件,當(dāng)我點(diǎn)擊他時(shí),注意大的那個(gè)事件也更新了,外面也有個(gè)藍(lán)框,不是很明顯,但是確實(shí)是更新了,在我后面調(diào)試打Log的時(shí)候也證實(shí)了這一點(diǎn)。所以在真實(shí)1400條數(shù)據(jù)下,被更新的還有另外1399個(gè)事件,這其實(shí)也是不必要的。

          我這里提到的事件和前文提到的預(yù)定是一個(gè)東西,react-big-calendar里面將這個(gè)稱為event,也就是事件,對(duì)應(yīng)我們業(yè)務(wù)的意義就是預(yù)定

          插播一個(gè)廣告,我也參加了掘金年度打榜活動(dòng)[6],各位看官給我投個(gè)票唄~ 謝謝各位了

          為什么會(huì)這樣?

          這個(gè)現(xiàn)象我好像似曾相識(shí),也是我們經(jīng)常會(huì)犯的一個(gè)性能上的問題:將一個(gè)狀態(tài)放到最頂層,然后一層一層往下傳,當(dāng)下面某個(gè)元素更新了這個(gè)狀態(tài),會(huì)導(dǎo)致根節(jié)點(diǎn)更新,從而觸發(fā)下面所有子節(jié)點(diǎn)的更新。這里說的更新并不一定要重新渲染DOM節(jié)點(diǎn),但是會(huì)運(yùn)行每個(gè)子節(jié)點(diǎn)的render函數(shù),然后根據(jù)render函數(shù)運(yùn)行結(jié)果來做diff,看看要不要更新這個(gè)DOM節(jié)點(diǎn)。React在這一步會(huì)幫我們省略不必要的DOM操作,但是render函數(shù)的運(yùn)行卻是必須的,而成千上萬次render函數(shù)的運(yùn)行也會(huì)消耗大量性能。

          說到這個(gè)我想起以前看到過的一個(gè)資料,也是講這個(gè)問題的,他用了一個(gè)一萬行的列表來做例子,原文在這里:high-performance-redux[7]。下面這個(gè)例子來源于這篇文章:

          function itemsReducer(state = initial_state, action) {  switch (action.type) {  case 'MARK':    return state.map((item) =>      action.id === item.id ?        {...item, marked: !item.marked } :        item    );  default:    return state;  }}
          class App extends Component { render() { const { items, markItem } = this.props; return (
          {items.map(item => )}
          ); }};
          function mapStateToProps(state) { return state;}
          const markItem = (id) => ({type: 'MARK', id});
          export default connect( mapStateToProps, {markItem})(App);

          上面這段代碼不復(fù)雜,就是一個(gè)App,接收一個(gè)items參數(shù),然后將這個(gè)參數(shù)全部渲染成Item組件,然后你可以點(diǎn)擊單個(gè)Item來改變他的選中狀態(tài),運(yùn)行效果如下:

          這段代碼所有數(shù)據(jù)都在items里面,這個(gè)參數(shù)從頂層App傳進(jìn)去,當(dāng)點(diǎn)擊Item的時(shí)候改變items數(shù)據(jù),從而更新整個(gè)列表。這個(gè)運(yùn)行結(jié)果跟我們上面的Calendar有類似的問題,當(dāng)單條Item狀態(tài)改變的時(shí)候,其他沒有涉及的Item也會(huì)更新。原因也是一樣的:頂層的參數(shù)items改變了。

          說實(shí)話,類似的寫法我見過很多,即使不是從App傳入,也會(huì)從其他大的組件節(jié)點(diǎn)傳入,從而引起類似的問題。當(dāng)數(shù)據(jù)量少的時(shí)候,這個(gè)問題不明顯,很多時(shí)候都被忽略了,像上面這個(gè)圖,即使一萬條數(shù)據(jù),因?yàn)槊總€(gè)Item都很簡(jiǎn)單,所以運(yùn)行一萬次render你也不會(huì)明顯感知出來,在控制臺(tái)看也就一百多毫秒。但是我們面臨的Calendar就復(fù)雜多了,每個(gè)子節(jié)點(diǎn)的運(yùn)算邏輯都更復(fù)雜,最終將我們的響應(yīng)速度拖累到了七八秒上。

          優(yōu)化方案

          還是先說這個(gè)一萬條的列表,原作者除了提出問題外,也提出了解決方案:頂層App只傳id,Item渲染的數(shù)據(jù)自己連接redux store獲取。下面這段代碼同樣來自這篇文章:

          // index.jsfunction items(state = initial_state, action) {  switch (action.type) {  case 'MARK':    const item = state[action.id];    return {      ...state,      [action.id]: {...item, marked: !item.marked}    };  default:    return state;  }}
          function ids(state = initial_ids, action) { return state;}
          function itemsReducer(state = {}, action) { return { // 注意這里,數(shù)據(jù)多了一個(gè)ids ids: ids(state.ids, action), items: items(state.items, action), }}
          const store = createStore(itemsReducer);
          export default class NaiveList extends Component { render() { return ( ); }}
          // app.jsclass App extends Component {  static rerenderViz = true;  render() {    // App組件只使用ids來渲染列表,不關(guān)心具體的數(shù)據(jù)    const { ids } = this.props;    return (      
          { ids.map(id => { return ; }) }
          ); }};
          function mapStateToProps(state) { return {ids: state.ids};}
          export default connect(mapStateToProps)(App);
          // Item.js// Item組件自己去連接Redux獲取數(shù)據(jù)class Item extends Component {  constructor() {    super();    this.onClick = this.onClick.bind(this);  }
          onClick() { this.props.markItem(this.props.id); }
          render() { const {id, marked} = this.props.item; const bgColor = marked ? '#ECF0F1' : '#fff'; return ( onClick={this.onClick} > {id}
          ); }}
          function mapStateToProps(_, initialProps) { const { id } = initialProps; return (state) => { const { items } = state; return { item: items[id], }; }}
          const markItem = (id) => ({type: 'MARK', id});
          export default connect(mapStateToProps, {markItem})(Item);

          這段代碼的優(yōu)化主要在這幾個(gè)地方:

          1.將數(shù)據(jù)從單純的items拆分成了idsitems2.頂層組件App使用ids來渲染列表,ids里面只有id,所以只要不是增加和刪除,僅僅單條數(shù)據(jù)的狀態(tài)變化,ids并不需要變化,所以App不會(huì)更新。3.Item組件自己去連接自己需要的數(shù)據(jù),當(dāng)自己關(guān)心的數(shù)據(jù)變化時(shí)才更新,其他組件的數(shù)據(jù)變化并不會(huì)觸發(fā)更新。

          拆解第三方庫源碼

          上面通過使用調(diào)試工具我看到了一個(gè)熟悉的現(xiàn)象,并猜到了他慢的原因,但是目前僅僅是猜測(cè),具體是不是這個(gè)原因還要看看他的源碼才能確認(rèn)。好在我在看他的源碼前先去看了下他的文檔[8],然后發(fā)現(xiàn)了這個(gè):

          react-big-calendar接收兩個(gè)參數(shù)onSelectEventselectedselected表示當(dāng)前被選中的事件(預(yù)定),onSelectEvent可以用來改變selected的值。也就是說當(dāng)我們選中某個(gè)預(yù)定的時(shí)候,會(huì)改變selected的值,由于這個(gè)參數(shù)是從頂層往下傳的,所以他會(huì)引起下面所有子節(jié)點(diǎn)的更新,在我們這里就是差不多7000個(gè)背景格子 + 1399個(gè)其他事件,這樣就導(dǎo)致不需要更新的組件更新了。

          頂層selected換成Context?

          react-big-calendar在頂層設(shè)計(jì)selected這樣一個(gè)參數(shù)是可以理解的,因?yàn)槭褂谜呖梢酝ㄟ^修改這個(gè)值來控制選中的事件。這樣選中一個(gè)事件就有了兩個(gè)途徑:

          1.用戶通過點(diǎn)擊某個(gè)事件來改變selected的值2.開發(fā)者可以在外部直接修改selected的值來選中某個(gè)事件

          有了前面一萬條數(shù)據(jù)列表優(yōu)化的經(jīng)驗(yàn),我們知道對(duì)于這種問題的處理辦法了:使用selected的組件自己去連接Redux獲取值,而不是從頂部傳入。可惜,react-big-calendar并沒有使用Redux,也沒有使用其他任何狀態(tài)管理庫。如果他使用Redux,我們還可以考慮添加一個(gè)action來給外部修改selected,可惜他沒有。沒有Redux就玩不轉(zhuǎn)了嗎?當(dāng)然不是!React其實(shí)自帶一個(gè)全局狀態(tài)共享的功能,那就是ContextReact Context API官方有詳細(xì)介紹[9]我之前的一篇文章也介紹過他的基本使用方法[10],這里不再講述他的基本用法,我這里想提的是他的另一個(gè)特性:使用Context Provider包裹時(shí),如果你傳入的value變了,會(huì)運(yùn)行下面所有節(jié)點(diǎn)的render函數(shù),這跟前面提到的普通props是一樣的。但是,如果Provider下面的兒子節(jié)點(diǎn)是PureComponent,可以不運(yùn)行兒子節(jié)點(diǎn)的render函數(shù),而直接運(yùn)行使用這個(gè)value的孫子節(jié)點(diǎn)

          什么意思呢,下面我將我們面臨的問題簡(jiǎn)化來說明下。假設(shè)我們只有三層,第一層是頂層容器Calendar,第二層是背景的空白格子(兒子),第三層是真正需要使用selected的事件(孫子):

          示例代碼如下:

          // SelectContext.js// 一個(gè)簡(jiǎn)單的Contextimport React from 'react'
          const SelectContext = React.createContext()
          export default SelectContext;
          // Calendar.js// 使用Context Provider包裹,接收參數(shù)selected,渲染背景Backgroundimport SelectContext from './SelectContext';
          class Calendar extends Component { constructor(...args) { super(...args)
          this.state = { selected: null };
          this.setSelected = this.setSelected.bind(this); }
          setSelected(selected) { this.setState({ selected }) }
          componentDidMount() { const { selected } = this.props;
          this.setSelected(selected); }
          render() { const { selected } = this.state; const value = { selected, setSelected: this.setSelected }
          return ( ) }}
          // Background.js// 繼承自PureComponent,渲染背景格子和事件Eventclass Background extends PureComponent {  render() {    const { events } = this.props;    return  (        
          這里面是7000個(gè)背景格子
          下面是渲染1400個(gè)事件 {events.map(event => )}
          ) }}
          // Event.js// 從Context中取selected來決定自己的渲染樣式import SelectContext from './SelectContext';
          class Event extends Component { render() { const { selected, setSelected } = this.context; const { event } = this.props;
          return (
          setSelected(event)}>
          ) }}
          Event.contextType = SelectContext; // 連接Context

          什么是PureComponent?

          我們知道如果我們想阻止一個(gè)組件的render函數(shù)運(yùn)行,我們可以在shouldComponentUpdate返回false,當(dāng)新的props相對(duì)于老的props來說沒有變化時(shí),其實(shí)就不需要運(yùn)行rendershouldComponentUpdate就可以這樣寫:

          shouldComponentUpdate(nextProps) {    const fields = Object.keys(this.props)    const fieldsLength = fields.length    let flag = false
          for (let i = 0; i < fieldsLength; i = i + 1) { const field = fields[i] if ( this.props[field] !== nextProps[field] ) { flag = true break } }
          return flag }

          這段代碼就是將新的nextProps與老的props一一進(jìn)行對(duì)比,如果一樣就返回false,不需要運(yùn)行render。而PureComponent其實(shí)就是React官方幫我們實(shí)現(xiàn)了這樣一個(gè)shouldComponentUpdate。所以我們上面的Background組件繼承自PureComponent,就自帶了這么一個(gè)優(yōu)化。如果Background本身的參數(shù)沒有變化,他就不會(huì)更新,而Event因?yàn)樽约哼B接了SelectContext,所以當(dāng)SelectContext的值變化的時(shí)候,Event會(huì)更新。這就實(shí)現(xiàn)了我前面說的如果Provider下面的兒子節(jié)點(diǎn)是PureComponent,可以不運(yùn)行兒子節(jié)點(diǎn)的render函數(shù),而直接運(yùn)行使用這個(gè)value的孫子節(jié)點(diǎn)

          PureComponent不起作用

          理想是美好的,現(xiàn)實(shí)是骨感的。。。理論上來說,如果我將中間兒子這層改成了PureComponent,背景上7000個(gè)格子就不應(yīng)該更新了,性能應(yīng)該大幅提高才對(duì)。但是我測(cè)試后發(fā)現(xiàn)并沒有什么用,這7000個(gè)格子還是更新了,什么鬼?其實(shí)這是PureComponent本身的一個(gè)問題:只進(jìn)行淺比較。注意this.props[field] !== nextProps[field],如果this.props[field]是個(gè)引用對(duì)象呢,比如對(duì)象,數(shù)組之類的?因?yàn)樗菧\比較,所以即使前后屬性內(nèi)容沒變,但是引用地址變了,這兩個(gè)就不一樣了,就會(huì)導(dǎo)致組件的更新!

          而在react-big-calendar里面大量存在這種計(jì)算后返回新的對(duì)象的操作,比如他在頂層Calendar里面有這種操作:

          代碼地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L790

          這行代碼的意思是每次props改變都去重新計(jì)算狀態(tài)state,而他的計(jì)算代碼是這樣的:

          代碼地址:https://github.com/jquense/react-big-calendar/blob/master/src/Calendar.js#L794

          注意他的返回值是一個(gè)新的對(duì)象,而且這個(gè)對(duì)象里面的屬性,比如localizer的計(jì)算方法mergeWithDefaults也是這樣,每次都返回新的對(duì)象:

          代碼地址:https://github.com/jquense/react-big-calendar/blob/master/src/localizer.js#L39

          這樣會(huì)導(dǎo)致中間兒子節(jié)點(diǎn)每次接受到的props雖然內(nèi)容是一樣的,但是因?yàn)槭且粋€(gè)新對(duì)象,即使使用了PureComponent,其運(yùn)行結(jié)果也是需要更新。這種操作在他的源碼中大量存在,其實(shí)從功能角度來說,這樣寫是可以理解的,因?yàn)槲矣袝r(shí)候也會(huì)這么干。。。有時(shí)候某個(gè)屬性更新了,不太確定要不要更新下面的組件,干脆直接返回一個(gè)新對(duì)象觸發(fā)更新,省事是省事了,但是面對(duì)我們這種近萬個(gè)組件的時(shí)候性能就崩了。。。

          歪門邪道shouldComponentUpdate

          如果只有一兩個(gè)屬性是這樣返回新對(duì)象,我還可以考慮給他重構(gòu)下,但是調(diào)試了一下發(fā)現(xiàn)有大量的屬性都是這樣,咱也不是他作者,也不知道會(huì)不會(huì)改壞功能,沒敢亂動(dòng)。但是不動(dòng)性能也繃不住啊,想來想去,還是在兒子的shouldComponentUpdate上動(dòng)點(diǎn)手腳吧。簡(jiǎn)單的this.props[field] !== nextProps[field]判斷肯定是不行的,因?yàn)橐玫刂纷兝玻撬麅?nèi)容其實(shí)是沒變,那我們就判斷他的內(nèi)容吧。兩個(gè)對(duì)象的深度比較需要使用遞歸,也可以參考React diff算法來進(jìn)行性能優(yōu)化,但是無論你怎么優(yōu)化這個(gè)算法,性能最差的時(shí)候都是兩個(gè)對(duì)象一樣的時(shí)候,因?yàn)樗麄兪且粯拥模阈枰闅v到最深處才能肯定他們是一樣的,如果對(duì)象很深,這種遞歸算法不見得會(huì)比運(yùn)行一遍render快,而我們面臨的大多數(shù)情況都是這種性能最差的情況。所以遞歸對(duì)比不太靠譜,其實(shí)如果你對(duì)這些數(shù)據(jù)心里有數(shù),沒有循環(huán)引用什么的,你可以考慮直接將兩個(gè)對(duì)象轉(zhuǎn)化為字符串來進(jìn)行對(duì)比,也就是

          JSON.stringify(this.props[field]) !== JSON.stringify(nextProps[field])

          注意,這種方式只適用于你對(duì)props數(shù)據(jù)了解,沒有循環(huán)引用,沒有變化的Symbol,函數(shù)之類的屬性,因?yàn)镴SON.stringify執(zhí)行時(shí)會(huì)丟掉Symbol和函數(shù),所以我說他是歪門邪道性能優(yōu)化

          將這個(gè)轉(zhuǎn)化為字符串比較的shouldComponentUpdate加到背景格子的組件上,性能得到了明顯增強(qiáng),點(diǎn)擊相應(yīng)速度從7.5秒下降到了5.3秒左右。

          按需渲染

          上面我們用shouldComponentUpdate阻止了7000個(gè)背景格子的更新,響應(yīng)時(shí)間下降了兩秒多,但是還是需要5秒多時(shí)間,這也很難接受,還需要進(jìn)一步優(yōu)化。按照我們之前說的如果還能阻止另外1399個(gè)事件的更新那就更好了,但是經(jīng)過對(duì)他數(shù)據(jù)結(jié)構(gòu)的分析,我們發(fā)現(xiàn)他的數(shù)據(jù)結(jié)構(gòu)跟我們前面舉的列表例子還不一樣。我們列表的例子所有數(shù)據(jù)都在items里面,是否選中是item的一個(gè)屬性,而react-big-calendar的數(shù)據(jù)結(jié)構(gòu)里面eventselectedEvent是兩個(gè)不同的屬性,每個(gè)事件通過判斷自己的event是否等于selectedEvent來判斷自己是否被選中。這造成的結(jié)果就是每次我們選中一個(gè)事件,selectedEvent的值都會(huì)變化,每個(gè)事件的屬性都會(huì)變化,也就是會(huì)更新,運(yùn)行render函數(shù)。如果不改這種數(shù)據(jù)結(jié)構(gòu),是阻止不了另外1399個(gè)事件更新的。但是改這個(gè)數(shù)據(jù)結(jié)構(gòu)改動(dòng)太大,對(duì)于一個(gè)第三方庫,我們又不想動(dòng)這么多,怎么辦呢?

          這條路走不通了,我們完全可以換一個(gè)思路,背景7000個(gè)格子,再加上1400個(gè)事件,用戶屏幕有那么大嗎,看得完嗎?肯定是看不完的,既然看不完,那我們只渲染他能看到部分不就可以了!按照這個(gè)思路,我們找到了一個(gè)庫:react-visibility-sensor[11]。這個(gè)庫使用方法也很簡(jiǎn)單:

          function MyComponent (props) {  return (          {({isVisible}) =>        
          I am {isVisible ? 'visible' : 'invisible'}
          } );}

          結(jié)合我們前面說的,我們可以將VisibilitySensor套在Background上面:

          class Background extends PureComponent {  render() {    return (              {({isVisible}) =>                  }          )  }}

          然后Event組件如果發(fā)現(xiàn)自己處于不可見狀態(tài),就不用渲染了,只有當(dāng)自己可見時(shí)才渲染:

          class Event extends Component {  render() {    const { selected } = this.context;    const { isVisible, event } = this.props;
          return ( { isVisible ? (
          復(fù)雜內(nèi)容
          ) : null} ) }}
          Event.contextType = SelectContext;

          按照這個(gè)思路我們又改了一下,發(fā)現(xiàn)性能又提升了,整體時(shí)間下降到了大概4.1秒:

          仔細(xì)看上圖,我們發(fā)現(xiàn)渲染事件Rendering時(shí)間從1秒左右下降到了43毫秒,快了二十幾倍,這得益于渲染內(nèi)容的減少,但是Scripting時(shí)間,也就是腳本執(zhí)行時(shí)間仍然高達(dá)4.1秒,還需要進(jìn)一步優(yōu)化。

          砍掉mousedown事件

          渲染這塊已經(jīng)沒有太多辦法可以用了,只能看看Scripting了,我們發(fā)現(xiàn)性能圖上鼠標(biāo)事件有點(diǎn)刺眼:

          一次點(diǎn)擊同時(shí)觸發(fā)了三個(gè)點(diǎn)擊事件:mousedownmouseupclick。如果我們能干掉mousedownmouseup是不是時(shí)間又可以省一半,先去看看他注冊(cè)這兩個(gè)事件時(shí)干什么的吧。可以直接在代碼里面全局搜mousedown,最終發(fā)現(xiàn)都是在Selection.js[12],通過對(duì)這個(gè)類代碼的閱讀,發(fā)現(xiàn)他是個(gè)典型的觀察者模式,然后再搜new Selection找到使用的地方,發(fā)現(xiàn)mousedownmouseup主要是用來實(shí)現(xiàn)事件的拖拽功能的,mousedown標(biāo)記拖拽開始,mouseup標(biāo)記拖拽結(jié)束。如果我把它去掉,拖拽功能就沒有了。經(jīng)過跟產(chǎn)品經(jīng)理溝通,我們后面是需要拖拽的,所以這個(gè)不能刪。

          事情進(jìn)行到這里,我也沒有更多辦法了,但是響應(yīng)時(shí)間還是有4秒,真是讓人頭大

          反正沒啥好辦法了,我就隨便點(diǎn)著玩,突然,我發(fā)現(xiàn)mousedown的調(diào)用棧好像有點(diǎn)問題:

          這個(gè)調(diào)用棧我用數(shù)字分成了三塊:

          1.這里面有很多熟悉的函數(shù)名啊,像啥performUnitOfWorkbeginWork,這不都是我在React Fiber這篇文章中提過的嗎?[13]所以這些是React自己內(nèi)部的函數(shù)調(diào)用2.render函數(shù),這是某個(gè)組件的渲染函數(shù)3.這個(gè)render里面又調(diào)用了renderEvents函數(shù),看起來是用來渲染事件列表的,主要的時(shí)間都耗在這里了

          mousedown監(jiān)聽本身我是干不掉了,但是里面的執(zhí)行是不是可以優(yōu)化呢?renderEvents已經(jīng)是庫自己寫的代碼了,所以可以直接全局搜,看看在哪里執(zhí)行的。最終發(fā)現(xiàn)是在TimeGrid.js[14]render函數(shù)被執(zhí)行了,其實(shí)這個(gè)是不需要執(zhí)行的,我們直接把前面歪門邪道的shouldComponentUpdate復(fù)制過來就可以阻止他的執(zhí)行。然后再看下性能數(shù)據(jù)呢:

          我們發(fā)現(xiàn)Scripting下降到了3.2秒左右,比之前減少約800毫秒,而mousedown的時(shí)間也從之前的幾百毫秒下降到了50毫秒,在圖上幾乎都看不到了,mouseup事件也不怎么看得到了,又算進(jìn)了一步吧~

          忍痛閹割功能

          到目前為止,我們的性能優(yōu)化都沒有閹割功能,響應(yīng)速度從7.5秒下降到了3秒多一點(diǎn),優(yōu)化差不多一倍。但是,目前這速度還是要三秒多,別說作為一個(gè)工程師了,作為一個(gè)用戶我都忍不了。咋辦呢?我們是真的有點(diǎn)黔驢技窮了。。。

          看看上面那個(gè)性能圖,主要消耗時(shí)間的有兩個(gè),一個(gè)是click事件,還有個(gè)timertimer到現(xiàn)在我還不知道他哪里來的,但是click事件我們是知道的,就是用戶點(diǎn)擊某個(gè)事件后,更改SelectContextselected屬性,然后selected屬性從頂層節(jié)點(diǎn)傳入觸發(fā)下面組件的更新,中間兒子節(jié)點(diǎn)通過shouldComponentUpdate跳過更新,孫子節(jié)點(diǎn)直接連接SelectContext獲取selected屬性更新自己的狀態(tài)。這個(gè)流程是我們前面優(yōu)化過的,但是,等等,這個(gè)貌似還有點(diǎn)問題。

          在我們的場(chǎng)景中,中間兒子節(jié)點(diǎn)其實(shí)包含了高達(dá)7000個(gè)背景格子,雖然我們通過shouldComponentUpdate跳過了render的執(zhí)行,但是7000個(gè)shouldComponentUpdate本省執(zhí)行也是需要時(shí)間的啊!有沒有辦法連shouldComponentUpdate的執(zhí)行也跳過呢?這貌似是個(gè)新的思路,但是經(jīng)過我們的討論,發(fā)現(xiàn)沒辦法在保持功能的情況下做到,但是可以適度閹割一個(gè)功能就可以做到,那閹割的功能是哪個(gè)呢?那就是暴露給外部的受控selected屬性!

          前面我們提到過選中一個(gè)事件有兩個(gè)途徑:

          1.用戶通過點(diǎn)擊某個(gè)事件來改變selected的值2.開發(fā)者可以在外部直接修改selected的值來選中某個(gè)事件

          之所以selected要放在頂層組件上就是為了實(shí)現(xiàn)第二個(gè)功能,讓外部開發(fā)者可以通過這個(gè)受控的selected屬性來改變選中的事件。但是經(jīng)過我們?cè)u(píng)估,外部修改selected這個(gè)并不是我們的需求,我們的需求都是用戶點(diǎn)擊來選中,也就是說外部修改selected這個(gè)功能我們可以不要。

          如果不要這個(gè)功能那就有得玩了,selected完全不用放在頂層了,只需要放在事件外層的容器上就行,這樣,改變selected值只會(huì)觸發(fā)事件的更新,啥背景格子的更新壓根就不會(huì)觸發(fā),那怎么改呢?在我們前面的Calendar -- Background -- Event模型上再加一層EventContainer,變成Calendar -- Background -- EventContainer -- EventSelectContext.Provider也不用包裹Calendar了,直接包裹EventContainer就行。代碼大概是這個(gè)樣子:

          // Calendar.js// Calendar簡(jiǎn)單了,不用接受selected參數(shù),也不用SelectContext.Provider包裹了class Calendar extends Component {  render() {    return (          )  }}
          // Background.js// Background要不要使用shouldComponentUpdate阻止更新可以看看還有沒有其他參數(shù)變化,因?yàn)閟elected已經(jīng)從頂層拿掉了// 改變selected本來就不會(huì)觸發(fā)Background更新// Background不再渲染單個(gè)事件,而是渲染EventContainerclass Background extends PureComponent {  render() {    const { events } = this.props;    return  (        
          這里面是7000個(gè)背景格子
          下面是渲染1400個(gè)事件
          ) }}
          // EventContainer.js// EventContainer需要SelectContext.Provider包裹// 代碼類似之前的Calendarimport SelectContext from './SelectContext';
          class EventContainer extends Component { constructor(...args) { super(...args)
          this.state = { selected: null };
          this.setSelected = this.setSelected.bind(this); }
          setSelected(selected) { this.setState({ selected }) }
          render() { const { selected } = this.state; const { events } = this.props; const value = { selected, setSelected: this.setSelected }
          return ( {events.map(event => )} ) }}
          // Event.js// Event跟之前是一樣的,從Context中取selected來決定自己的渲染樣式import SelectContext from './SelectContext';
          class Event extends Component { render() { const { selected, setSelected } = this.context; const { event } = this.props;
          return (
          setSelected(event)}>
          ) }}
          Event.contextType = SelectContext; // 連接Context

          這種結(jié)構(gòu)最大的變化就是當(dāng)selected變化的時(shí)候,更新的節(jié)點(diǎn)是EventContainer,而不是頂層Calendar,這樣就不會(huì)觸發(fā)Calendar下其他節(jié)點(diǎn)的更新。缺點(diǎn)就是Calendar無法從外部接收selected了。

          需要注意一點(diǎn)是,如果像我們這樣EventContainer下面直接渲染Event列表,selected不用Context也可以,可以直接作為EventContainerstate但是如果EventContainerEvent中間還有層級(jí),需要穿透?jìng)鬟f,仍然需要Context,中間層級(jí)和以前的類似,使用shouldComponentUpdate阻止更新

          還有一點(diǎn),因?yàn)?code style="box-sizing: border-box;padding: 3px 5px;color: rgb(255, 53, 2);line-height: 1.5;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 14.4px;background: rgb(248, 245, 236);border-radius: 2px;">selected不在頂層了,所以selected更新也不會(huì)觸發(fā)中間Background更新了,所以Background上的shouldComponentUpdate也可以刪掉了。

          我們這樣優(yōu)化后,性能又提升了:

          現(xiàn)在Scripting時(shí)間直接從3.2秒降到了800毫秒,其中click事件只有163毫秒,現(xiàn)在從我使用來看,卡頓已經(jīng)不明顯了,直接錄個(gè)動(dòng)圖來對(duì)比下吧:

          【動(dòng)圖太大,微信公眾號(hào)不讓發(fā),可以點(diǎn)擊“閱讀原文”去掘金看。

          上面這個(gè)動(dòng)圖已經(jīng)基本看不出卡頓了,但是我們性能圖上為啥還有800毫秒呢,而且有一個(gè)很長(zhǎng)的Timer Fired。經(jīng)過我們的仔細(xì)排查,發(fā)現(xiàn)這其實(shí)是個(gè)烏龍,Timer Fired在我一開始錄制性能就出現(xiàn)了,那時(shí)候我還在切換頁面,還沒來得及點(diǎn)擊呢,如果我們點(diǎn)進(jìn)去會(huì)發(fā)現(xiàn)他其實(shí)是按需渲染引入的react-visibility-sensor的一個(gè)檢查元素可見性的定時(shí)任務(wù),并不是我們點(diǎn)擊事件的響應(yīng)時(shí)間。把這塊去掉,我們點(diǎn)擊事件的響應(yīng)時(shí)間其實(shí)不到200毫秒。

          從7秒多優(yōu)化到不到200毫秒,三十多倍的性能優(yōu)化,終于可以交差了,哈哈??

          總結(jié)

          本文分享的是我工作中實(shí)際遇到的一個(gè)案例,實(shí)現(xiàn)的效果是將7秒左右的響應(yīng)時(shí)間優(yōu)化到了不到200毫秒,優(yōu)化了三十幾倍,優(yōu)化的代價(jià)是犧牲了一個(gè)不常用的功能。

          本來想著要是優(yōu)化好了可以給這個(gè)庫提個(gè)PR,造福大家的。但是優(yōu)化方案確實(shí)有點(diǎn)歪門邪道:

          1.使用了JSON.stringify來進(jìn)行shouldComponentUpdate的對(duì)比優(yōu)化,對(duì)于函數(shù),Symbol屬性的改變沒法監(jiān)聽到,不適合開放使用,只能在數(shù)據(jù)自己可控的情況下小規(guī)模使用。2.犧牲了一個(gè)暴露給外部的受控屬性selected,破壞了功能。

          基于這兩點(diǎn),PR我們就沒提了,而是將修改后的代碼放到了自己的私有NPM倉庫。

          下面再來總結(jié)下本文面臨的問題和優(yōu)化思路:

          遇到的問題

          我們需求是要做一個(gè)體育場(chǎng)館的管理日歷,所以我們使用了react-big-calendar這個(gè)庫。我們需求的數(shù)據(jù)量是渲染7000個(gè)背景格子,然后在這個(gè)背景格子上渲染1400個(gè)事件。這近萬個(gè)組件渲染后,我們發(fā)現(xiàn)僅僅一次點(diǎn)擊就需要7秒多,完全不能用。經(jīng)過細(xì)致排查,我們發(fā)現(xiàn)慢的原因是點(diǎn)擊事件的時(shí)候會(huì)改變一個(gè)屬性selected。這個(gè)屬性是從頂層傳下來的,改變后會(huì)導(dǎo)致所有組件更新,也就是所有組件都會(huì)運(yùn)行render函數(shù)。

          第一步優(yōu)化

          為了阻止不必要的render運(yùn)行,我們引入了Context,將selected放到Context上進(jìn)行透?jìng)鳌V虚g層級(jí)因?yàn)椴恍枰褂?code style="box-sizing: border-box;padding: 3px 5px;color: rgb(255, 53, 2);line-height: 1.5;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 14.4px;background: rgb(248, 245, 236);border-radius: 2px;">selected屬性,所以可以使用shouldComponentUpdate來阻止render的運(yùn)行,底層需要使用selected的組件自行連接Context獲取。

          第一步優(yōu)化的效果

          響應(yīng)時(shí)間從7秒多下降到5秒多。

          第一步優(yōu)化的問題

          底層事件仍然有1400個(gè),獲取selected屬性后,1400個(gè)組件更新仍然要花大量的時(shí)間。

          第二步優(yōu)化

          為了減少點(diǎn)擊后更新的事件數(shù)量,我們?yōu)槭录氚葱桎秩荆讳秩居脩艨梢姷氖录M件。同時(shí)我們還對(duì)mousedownmouseup進(jìn)行了優(yōu)化,也是使用shouldComponentUpdate阻止了不必要的更新。

          第二步優(yōu)化效果

          響應(yīng)時(shí)間從5秒多下降到3秒多。

          第二步優(yōu)化的問題

          響應(yīng)時(shí)間仍然有三秒多,經(jīng)過分析發(fā)現(xiàn),背景7000個(gè)格子雖然使用shouldComponentUpdate阻止了render函數(shù)的運(yùn)行,但是shouldComponentUpdate本身運(yùn)行7000次也要費(fèi)很長(zhǎng)時(shí)間。

          第三步優(yōu)化

          為了讓7000背景格子連shouldComponentUpdate都不運(yùn)行,我們?nèi)掏撮幐盍隧攲邮芸氐?code style="box-sizing: border-box;padding: 3px 5px;color: rgb(255, 53, 2);line-height: 1.5;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;font-size: 14.4px;background: rgb(248, 245, 236);border-radius: 2px;">selected屬性,直接將它放到了事件的容器上,它的更新再也不會(huì)觸發(fā)背景格子的更新了,也就是連shouldComponentUpdate都不運(yùn)行了。

          第三步優(yōu)化效果

          響應(yīng)時(shí)間從3秒多下降到不到200毫秒。

          第三步優(yōu)化的問題

          功能被閹割了,其他完美!

          參考資料:

          react-big-calendar倉庫[15]

          high-performance-redux[16]


          覺得博主寫得還可以的話,不要忘了分享、點(diǎn)贊、在看三連哦~

          長(zhǎng)按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~?

          References

          [1]?react-big-calendar:?https://github.com/jquense/react-big-calendar
          [2]?速度提高幾百倍,記一次數(shù)據(jù)結(jié)構(gòu)在實(shí)際工作中的運(yùn)用:?https://juejin.cn/post/6898569107877134350
          [3]?使用mono-repo實(shí)現(xiàn)跨項(xiàng)目組件共享:?https://juejin.cn/post/6913788953971654663
          [4]?resources:?http://jquense.github.io/react-big-calendar/examples/index.html#prop-resources
          [5]?React Developer Tools:?https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
          [6]?掘金年度打榜活動(dòng):?https://rank.juejin.cn/?u=%E8%92%8B%E9%B9%8F%E9%A3%9E&t=user
          [7]?high-performance-redux:?http://somebody32.github.io/high-performance-redux/
          [8]?他的文檔:?http://jquense.github.io/react-big-calendar/examples/index.html#api
          [9]?官方有詳細(xì)介紹:?https://reactjs.org/docs/context.html
          [10]?我之前的一篇文章也介紹過他的基本使用方法:?https://juejin.cn/post/6847902222756347911#heading-1
          [11]?react-visibility-sensor:?https://www.npmjs.com/package/react-visibility-sensor
          [12]?Selection.js:?https://github.com/jquense/react-big-calendar/blob/master/src/Selection.js
          [13]?React Fiber這篇文章中提過的嗎?:?https://juejin.cn/post/6844904197008130062
          [14]?TimeGrid.js:?https://github.com/jquense/react-big-calendar/blob/master/src/TimeGrid.js
          [15]?react-big-calendar倉庫:?https://github.com/jquense/react-big-calendar
          [16]?high-performance-redux:?http://somebody32.github.io/high-performance-redux/
          [17]?進(jìn)擊的大前端:?https://test-dennis.oss-cn-hangzhou.aliyuncs.com/QRCode/QR430.jpg


          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色在线观看有限公司jb啊啊相当到位 | 无码人妻一区二区三区综合另类 | 以及视频片又粗又猛 | 黄色一级片播放视频 | 插菊花综合网1 |