React 移動 web 極致優化

原文地址:https://github.com/lcxfs1991/...node

最近一個季度,咱們都在爲手Q家校羣作重構優化,將原有那套問題不斷的框架換掉。通過一些斟酌,決定使用react 進行重構。選擇react,其實也主要是由於它具備下面的三大特性。react

React的特性

1. Learn once, write anywhere

學習React的好處就是,學了一遍以後,可以寫web, node直出,以及native,可以適應各類紛繁複雜的業務。須要輕量快捷的,直接能夠用Reactjs;須要提高首屏時間的,能夠結合React Server Render;須要更好的性能的,能夠上React Native。webpack

可是,這其實暗示學習的曲線很是陡峭。單單是Webpack+ React + Redux就已夠一個入門者夠嗆,更況且還要兼顧直出和手機客戶端。不是通常人能hold住全部端。git

2. Virtual Dom

Virtual Dom(下稱vd)算是React的一個重大的特點,由於Facebook宣稱因爲vd的幫助,React可以達到很好的性能。是的,Facebook說的沒錯,但只說了一半,它說漏的一半是:「除非你能正確的採用一系列優化手段」。es6

3. 組件化

另外一個被你們所推崇的React優點在於,它能令到你的代碼組織更清晰,維護起來更容易。咱們在寫的時候也有同感,但那是直到咱們踩了一些坑,而且漸漸熟悉React+ Redux所推崇的那套代碼組織規範以後。github

那麼?

上面的描述難免有些先揚後抑的感受,那是由於每每做爲React的剛入門者,都會像咱們初入的時候同樣,對React滿懷但願,指意它幫咱們作好一切,但隨着瞭解的深刻,發現須要作一些額外的事情來達到咱們的期待。web

對React的期待

初學者對React可能滿懷期待,以爲React可能完爆其它一切框架,甚至不切實際地認爲React可能連原生的渲染都能完爆——對框架的狂熱確實會出現這樣的不切實際的期待。讓咱們來看看React的官方是怎麼說的。React官方文檔在Advanced Performanec這一節,這樣寫道:算法

One of the first questions people ask when considering React for a project is whether their application will be as fast and responsive as an equivalent non-React version

顯然React本身也其實只是想盡可能達到跟非React版本相若的性能。React在減小重複渲染方面確實是有一套獨特的處理辦法,那就是vd,但顯示在首次渲染的時候React絕無可能超越原生的速度,或者必定能將其它的框架比下去。所以,咱們在作優化的時候,可的期待的東西有:chrome

  • 首屏時間可能會比較原生的慢一些,但能夠嘗試用React Server Render (又稱Isomorphic)去提升效率redux

  • 用戶進行交互的時候,有可能會比原生的響應快一些,前提是你作了一些優化避免了浪費性能的重複渲染。

以手Q家校羣功能頁React重構優化爲例

手Q家校羣功能頁主要由三個頁面構成,分別是列表頁、佈置頁和詳情頁。列表頁已經重構完成並已發佈,佈置頁已重構完畢準備提測,詳情頁正在重構。與此同時咱們已完成對列表頁的同構直出優化,並已正在作React Native優化的鋪墊。

這三個頁面的重構其實覆蓋了很多頁面的案例,因此仍是蠻有表明性的,咱們會將重構之中遇到的一些經驗穿插在文章裏論述。

在手Q家校羣重構以前,其實咱們已經作了一版PC家校羣。當時將native的頁面所有web化,直接就採用了React比較經常使用的全家桶套裝:

  • 構建工具 => gulp + webpack

  • 開發效率提高 => redux-dev-tools + hot-reload

  • 統一數據管理=> redux

  • 性能提高 => immutable + purerender

  • 路由控制器 => react-router(手Q暫時沒采用)

爲何咱們在優化的時候主要講手Q呢?畢竟PC的性能在大部份狀況下已經很好,在PC上一些存在的問題都被PC良好的性能掩蓋下去。手機的性能不如PC,所以有更多有價值的東西深挖。開發的時候我就跟同事開玩笑說:「沒作過手機web優化的都真很差意思說本身作過性能優化啊「。

構建針對React作的優化

我在《性能優化三部曲之一——構建篇》提出,「經過構建,咱們能夠達成開發效率的提高,以及對項目最基本的優化」。在進行React重構優化的過程當中,構建對項目的優化做用必不可少。在本文暫時不贅述,我另外開闢了一篇《webpack使用優化(react篇)》進行具體論述。

開發效率提高工具

1

在PC端使用Redux的時候,咱們都很喜歡使用Redux-Devtools來查看Redux觸發的action,以及對應的數據變化。PC端使用的時候,咱們習慣擺在右邊。但移動端的屏幕較少,所以家校羣項目使用的時候放在底部,並且因爲性能問題,咱們在constant裏設一個debug參數,而後在chrome調試時打開,移動端非必須的時候關閉。不然,它會致使移動web的渲染比較低下。

數據管理及性能優化

Redux統一管理數據

這一部份算是重頭戲吧。React做爲View層的框架,已經經過vd幫助咱們解決重複渲染的問題。但vd是經過看數據的先後差別去判斷是否要重複渲染的,但React並無幫助咱們去作這層比較。所以咱們須要使用一整套數據管理工具及對應的優化方法去達成。在這方法,咱們選擇了Redux。

Redux整個數據流大致能夠用下圖來描述:

2

Redux這個框架的好處在於可以統一在本身定義的reducer函數裏面去進行數據處理,在View層中只須要經過事件去處觸發一些action就能夠改變地應的數據,這樣可以使數據處理和dom渲染更好地分離,而避免手動地去設置state。

在重構的時候,咱們傾向於將功能相似的數據歸類到一塊兒,並創建對應的reducer文件對數據進行處理。以下圖,是手Q家校羣佈置頁的數據結構。有些大型的SPA項目可能會將初始數據分開在不一樣的reducer文件裏,但這裏咱們傾向於歸到一個store文件,這樣可以清晰地知道整個文件的數據結構,也符合Redux想統一管理數據的想法。而後數據的每一個層級與reducer文件都是一一對應的關係。

3

重複渲染致使卡頓

這套React + Redux的東西在PC家校羣頁面上用得很歡樂, 以致於不用怎麼寫shouldComponentUpdate都沒遇到過什麼性能問題。但放到移動端上,咱們在列表頁重構的時候就立刻遇到卡頓的問題了。

什麼緣由呢?是重複渲染致使的!!!!!!

說好的React vd能夠減小重複渲染呢?!!!

請別忘記前提條件!!!!

你能夠在每一個component的render裏,放一個console.log("xxx component")。而後觸發一個action,在優化以前,幾乎所有的component都打出這個log,代表都重複渲染了。

React性能的救星Immutablejs

4
(網圖,引用的文章太多以至於不知道哪篇纔是出處)

上圖是React的生命週期,還沒熟悉的同窗能夠去熟悉一下。由於其中的shouldComponentUpdate是優化的關鍵。React的重複渲染優化的核心其實就是在shouldComponentUpdate裏面作數據比較。在優化以前,shouldComponentUpdate是默認返回true的,這致使任什麼時候候觸發任何的數據變化都會使component從新渲染。這必然會致使資源的浪費和性能的低下——你可能會感受比較原生的響應更慢。

這時你開始懷疑這世界——是否是Facebook在騙我。

當時遇到這個問題個人開始翻閱文檔,也是在Facebook的Advanced Performance一節中找到答案:Immutablejs。這個框架已被吹了有一年多了吧,吹這些框架的人理解它的原理,但不必定實踐過——由於做爲一線移動端開發者,打開它的github主頁看dist文件,50kb,我就已經打退堂鼓了。只是遇到了性能問題,咱們纔再認真地去了解一遍。

Immutable這個的意思就是不可變,Immutablejs就是一個生成數據不可變的框架。一開始你並不理解不可變有什麼用。最開始的時候Immutable這種數據結構是爲了解決數據鎖的問題,而對於js,就能夠借用來解決先後數據比較的問題——由於同時Immutablejs還提供了很好的數據比較方法——Immutable.is()。小結一下就是:

  • Immutablejs自己就能生成不可變數據,這樣就不須要開發者本身去作數據深拷貝,能夠直接拿prevProps/prevState和nextProps/nextState來比較。

  • Immutable自己還提供了數據的比較方法,這樣開發者也不用本身去寫數據深比較的方法。

說到這裏,已萬事俱備了。那東風呢?咱們還欠的東風就是應該在哪裏寫這個比較。答案就是shouldComponentUpdate。這個生命週期會傳入nextProps和nextState,能夠跟component當前的props和state直接比較。這個就能夠參考pure-render的作法,去重寫shouldComponentUpdate,在裏面寫數據比較的邏輯。

其中一位同事polarjiang利用Immutablejs的is方法,參考pure-render-decorator寫了一個immutable-pure-render-decorator

那具體怎麼使用immutable + pure-render呢?

對於immutable,咱們須要改寫一下reducer functions裏面的處理邏輯,一概換成Immutable的api。

至於pure-render,如果es5寫法,能夠用使mixin;如果es6/es7寫法,須要使用decorator,在js的babel loader裏面,新增plugins: [‘transform-decorators-legacy’]。其es6的寫法是

@pureRender
export default class List extends Component { ... }

Immutablejs帶來的一些問題

不從新渲染

你可能會想到Immutable能減小無謂的從新渲染,但可能沒想過會致使頁面不能正確地從新渲染。目前列表頁在老師進入的時候是有2個tab的,tab的切換會讓列表也切換。目前手Q的列表頁學習PC的列表頁,兩個列表共用一套dom結構(由於除了做業佈置者名字以外,兩個列表如出一轍)。上了Immutablejs以後,當碰巧「我發佈的「列表和」所有「列表開頭的幾個做業都是同一我的佈置的時候,列表切換就不從新渲染了。

引入immutable和pureRender後,render裏的JSX注意必定不要有一樣的key(如兩個列表,有重複的數據,此時以數據id來做爲key就不太合適,應該要用數據id + 列表類型做爲key),會形成不渲染新數據狀況。列表頁目前的處理辦法是將key值換成id + listType。

4
(列表頁兩個列表的切換)

這樣寫除了保證在父元素那一層知曉數據(key值)不一樣須要從新渲染以外,也保證了React底層渲染知道這是兩組不一樣的數據。在React源文件裏有一個ReactChildReconciler.js主要是寫children的渲染邏輯。其中的updateChildren裏面有具體如何比較先後children,而後再決定是否要從新渲染。在比較的時候它調用了shouldUpdateReactComponent方法。咱們看到它有對key值作比較。在兩個列表中有不一樣的key,在數據類似的狀況下,能保證二者切換的時候能從新渲染。

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return nextType === 'string' || nextType === 'number';
  } else {
    return nextType === 'object' && prevElement.type === nextElement.type && prevElement.key === nextElement.key;
  }
}

Immutablejs太大了

上文也提到Immutablejs編譯後的包也有50kb。對於PC端來講可能無所謂,網速足夠快,但對於移動端來講壓力就大了。有人寫了個seamless-immutable,算是簡易版的Immutablejs,只有2kb,只支持Object和Array。

但其實數據比較邏輯寫起來也並不難,所以再去review代碼的時候,我決定嘗試本身寫一個,也是這個決定讓我發現了更多的奧祕。

針對React的這個數據比較的深比較deepCompare,要點有2個:

  • 儘可能使傳入的數據扁平化一點

  • 比較的時候作一些限制,避免溢出棧

先上一下列表頁的代碼,以下圖。這裏當時是學習了PC家校羣的作法,將component做爲props傳入。這裏的<Scroll>封裝的是滾動檢測的邏輯,而<List>則是列表頁的渲染,<Empty>是列表爲空的時候展現的內容,<Loading>是列表底部加載的顯示橫條。

5

針對deepCompare的第1個要點,扁平化數據,咱們很明顯就能定位出其中一個問題了。例如<Empty>,咱們傳入了props.hw,這個props包括了兩個列表的數據。但這樣的結構就會是這樣

props.hw = {
    listMine: [
        {...}, {...}, ...
    ],
    listAll: [
        {...}, {...}, ...
    ],
}

但若是咱們提早在傳入以前判斷當前在哪一個列表,而後傳入對應列表的數量,則會像這樣:
props.hw = 20;

二者比較起來,顯示是後者簡單得多。

針對deepCompare第2點,限制比較的條件。首先讓咱們想到的是比較的深度。通常而言,對於Object和Array數據,咱們都須要遞歸去進行比較,出於性能的考慮,咱們都會限制比較的深度。

除此以外,咱們回顧一下上面的代碼,咱們將幾個React component做爲props傳進去了,這會在shouldComponentUpdate裏面顯示出來。這些component的結構大概以下:

6

$$typeof // 類型
_owner // 父組件
_self: // 僅開發模式出現
_source: //  僅開發模式出現
_store //  僅開發模式出現
key // 組件的key屬性值
props // 從傳入的props
ref // 組件的ref屬性值
type 本組件ReactComponent

所以,針對component的比較,有一些是能夠忽略的,例如$$typeof, _store, _self, _source, _ownertype這個比較複雜,能夠比較,但僅限於咱們定好的比較深度。若是不作這些忽略,這個深比較將會比較消耗性能。關於這個deepCompare的代碼,我放在了pure-render-deepCompare-decorator

不過其實,將component看成props傳入更爲靈活,並且可以增長組件的複用性,但從上面看來,是比較消耗性能的。看了官方文檔以後,咱們嘗試換種寫法,主要就是採用<Scroll>包裹<List>的作法,而後用this.props.children在<Scroll>裏面渲染,並將<Empty>, <Loading>抽出來。

7

8

本覺得React可能會對children這個props有什麼特殊處理,但它依然是將children看成props,傳入shouldComponentUpdate,這就迫使父元素<Scroll>要去判斷是否要從新渲染,進而跳到子無素<List>再去判斷是否進一步進行渲染。

<Scroll>究竟要不要去作這重判斷呢?針對列表頁這種狀況,咱們以爲能夠暫時不作,因爲<Scroll>包裹的元素很少,<Scroll>能夠先重複渲染,而後再交由子元素<List>本身再去判斷。這樣咱們對pure-render-deepCompare-decorator要進行一些修改,當輪到props.children判斷的時候,咱們要求父元素直接從新渲染,這樣就能交給子元素去作下一步的處理。

若是<Scroll>包裹的只有<List>還好,若是還有像<Empty>, <Loading>甚至其它更多的子元素,那<Scroll>從新渲染會觸發其它子元素去運算,判斷本身是否要作從新渲染,這就形成了浪費。react的官方論壇上已經有人提出,React的將父子元素的重複渲染的決策都放在shouldComponentUpdate,可能致使了耦合Shouldcomponentupdate And Children

lodash.merge能夠解決大部份場景

此段更新於2016年6月30日
因爲immutable的大小問題一直縈繞頭上,久久不得散去,所以再去找尋其它的方案。後面決定嘗試一下lodash.merge,並用上以前本身寫的pureRender。在渲染性能上還能夠接受,在僅比immutable差一點點(後面會披露具體數據),但卻帶來了30kb的減包。

性能優化小Tips

這裏概括了一些其它性能優化的小Tips

請慎用setState,因其容易致使從新渲染

既然將數據主要交給了Redux來管理,那就儘可能使用Redux管理你的數據和狀態state,除了少數狀況外,別忘了shouldComponentUpdate也須要比較state。

請將方法的bind一概置於constructor

Component的render裏不動態bind方法,方法都在constructor裏bind好,若是要動態傳參,方法可以使用閉包返回一個最終可執行函數。如:showDelBtn(item) { return (e) => {}; }。若是每次都在render裏面的jsx去bind這個方法,每次都要綁定會消耗性能。

請只傳遞component須要的props

傳得太多,或者層次傳得太深,都會加劇shouldComponentUpdate裏面的數據比較負擔,所以,也請慎用spread attributes(<Component {...props} />)。

請儘可能使用const element

這個用法是工業聚在React討論微信羣裏教會的,咱們能夠將不怎麼變更,或者不須要傳入狀態的component寫成const element的形式,這樣能加快這個element的初始渲染速度。

路由控制與拆包

當項目變得更大規模與複雜的時候,咱們須要設計成SPA,這時路由管理就很是重要了,這使特定url參數可以對應一個頁面。

9

PC家校羣整個設計是一箇中型的SPA,當js bundle太大的時候,須要拆分紅幾個小的bundle,進行異步加載。這時能夠用到webpack的異步加載打包功能,require。

10

在重構手Q家校羣佈置頁的時候,咱們有很多的浮層,列表有佈置頁內容主浮層、同步到多羣浮層、科目管理浮層以及指定羣成員浮層。這些徹底可使用react-router進行管理。可是因爲當時一早使用了Immutablejs,js bundle已經比較大,咱們就不打算使用react-router了。但後面仍然發現包比重構前要大一些,所以爲了保證首屏時間不慢於重構前,咱們但願在不用react-router的狀況下進行分包,其實也並不難,以下面2幅圖:

12

11

首先在切換浮層方法裏面,使用require.ensure,指定要加載哪一個包。
在setComponent方法裏,將component存在state裏面。
在父元素的渲染方法裏,當state有值的時候,就會自動渲染加載回來的component。

性能數據

首屏可交互時間

目前只有列表頁發佈外網了,咱們比較了優化先後的首屏可交互時間,分別有18%和5.3%的提高。

13

14

渲染FPS

更新於2016年7月2日

Android

React重構後初版,當時還沒作任何的優化,發現平均FPS只有22(雖然Android的肉眼感覺不出來),然後面使用Immutable或者Lodash.merge都很是接近,能達到42或以上。而手機QQ可接受的FPS最少值是30FPS。所以使用Immutable和Lodash.merge的優化仍是至關明顯的。

  • 重構後初版
    before rebuild

  • Immutable
    Immutable

  • Lodash.merge
    Lodash.merge

iOS

在iOS上的fps差距尤其明顯。重構後初版,拉了大概5屏以後,肉眼會有卡頓的感受,拉到了10屏以後,數據開始掉到了20多30。而Immutable和Lodash.merge則大部份時間保持在50fps以上,不少時候還能達到很是流暢的60fps。

  • 重構後初版
    before rebuild

  • Immutable
    Immutable

  • Lodash.merge
    Lodash.merge

Chrome模擬器

用Chrome模擬器也能看出一些端倪。在Scripting方面,Immutable和Lodash.merge的耗時是最少的,約700多ms,而重構後的初版則須要1220ms。Lodash.merge在rendering和painting上則沒佔到優點,但Immutable則要比其它兩個要少30% - 40%。因爲測試的時候是在PC端,PC端的性能又極好,因此無論是肉眼,仍是數據,對於不是很複雜的需求,整體的渲染性能看不出很是明顯的差距。

  • 重構後初版
    before rebuild

  • Immutable
    Immutable

  • Lodash.merge
    Lodash.merge

從上面的數據看來,在移動端使用Immutable和Lodash.merge相對於不用,會有較大的性能優點,但Immutable相對於Lodash.merge在咱們需求情景下暫時沒看出明顯的優點,筆者估計多是因爲項目數據規模不大,結構不復雜,所以Immutable的算法優點並無充分發揮出來。

測試註明

Android端測試FPS是使用了騰訊開發的GT隨身調。而iOS則使用了Macbook裏xCode自帶的instrument中的animation功能。Chrome模擬器則使用了Chrome的timeline。測試的方式是勻速滾動列表,拉出數據進行渲染。

React性能優化軍規

咱們在開發的過程當中,將上面所論述的內容,總結成一個基本的軍規,銘記於心,就能夠保證React應用的性能不至於太差。

渲染相關

  • 提高級項目性能,請使用immutable(props、state、store)

  • 請pure-render-decorator與immutablejs搭配使用

  • 請慎用setState,因其容易致使從新渲染

  • 謹慎將component看成props傳入

  • 請將方法的bind一概置於constructor

  • 請只傳遞component須要的props,避免其它props變化致使從新渲染(慎用spread attributes)

  • 請在你但願發生從新渲染的dom上設置可被react識別的同級惟一key,不然react在某些狀況可能不會從新渲染。

  • 請儘可能使用const element

tap事件

1. 簡單的tap事件,請使用react-tap-event-plugin

開發環境時,最好引入webpack的環境變量(僅在開發環境中初始化),在container中初始化。生產環境的時候,請將plugin跟react打包到一塊兒(須要打包在一塊兒才能正常使用,由於plugin對react有好多依賴),外鏈引入。

目前參考了這個項目的打包方案:

2. 複雜的tap事件,建議使用tap component

家校羣列表頁的每一個做業的tap交互都比較複雜,出了普通的tap以外,還須要long tap和swipe。所以咱們只好本身封裝了一個tap component

Debug相關

  • 移動端請慎用redux-devtools,易形成卡頓

  • Webpack慎用devtools的inline-source-map模式
    使用此模式會內聯一大段便於定位bug的字符串,查錯時能夠開啓,不是查錯時建議關閉,不然開發時加載的包會很是大。

其它

  • 慎用太新的es6語法。
    Object.assign等較新的類庫避免在移動端上使用,會報錯。

Object.assign目前使用object-assign包。或者使用babel-plugin-transform-object-assign插件。會轉換成一個extends的函數:

var _extends = ...;

_extends(a, b);

若有錯誤,請斧正!

相關文章
相關標籤/搜索