React + Reflux 渲染性能優化原理

做者:ManfredHu
連接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinciple
聲明:版權全部,轉載請保留本段信息,不然請不要轉載css

React

React

React的優勢有不少,如今不少應用都接入React這個框架。
在我看來,有下列優勢:
- Facebook團隊研發並維護——有團隊維護更新且有質量保證
- 在MVVM結構下只起View的做用——簡單接入,不須要花費大量人力重構代碼
- 組件化形式構建Web應用——複用性強,提升開發效率
- 用Virtual DOM減小對DOM的頻繁操做提升頁面性能——批量操做減小重排(reflows)和重繪(repaints)次數——性能對比舊的方式有提升html

React對重排和重繪的提升

雅虎性能優化比較重要的點,老司機自行忽略。
以下圖,HTML被瀏覽器解析爲DOM樹,CSS代碼加載進來解析爲樣式結構體,二者關聯組成渲染樹,以後瀏覽器把渲染樹繪製出來就是咱們看到的網頁了。這裏若是咱們對DOM樹或者樣式結構體作一些操做,如刪除某個節點,樣式改成隱藏(display:none)等等,會觸發重排進而致使重繪。
重排與重繪前端

觸發重排的條件

  • DOM元素的數量屬性變化
  • DOM樹的結構變化——節點的增減、移動
  • 某些佈局屬性的讀取和設置觸發重排——offsetTop/offsetWidth/scrollTop等等
    致使子級、後續兄弟元素、父節點因從新計算佈局而重排

觸發重繪的條件

  • 簡單樣式屬性的變化——顏色、背景色等
  • 重排致使的重繪

而React維護了一個Virtual DOM將短期的操做合併起來一塊兒同步到DOM,因此這也是它對整個前端領域提出的最重要的改變。node

爲何引入Reflux?

上面說了React在MVVM結構下只起View的做用,那麼除了View,MVVM下還有Model,ViewModel。
而純粹的View,會讓整個邏輯耦合在一層下,數據也須要層層傳遞,不方便控制和複用。
組件化遇到的問題react

故業內也有一堆的分層框架——如最先的flux,如今部門在用的Reflux,以及Redux。
對比Redux,Reflux更容易理解和上手——這也是現狀,學習成本越低,接入現有業務就越容易。linux

Reflux

reflux的架構很是簡單,就是三部分git

  1. Action 理解爲一個命令或者動做,經過它來向組件發出」指令」
  2. Store 爲ViewModel部分,組件的一些狀態屬性會存儲在這裏
  3. View Component 爲組件模板
    reflux的架構

因此Reflux只是讓咱們,更好的去操做組件,經過一個Action命令,叫組件去幹嗎,組件本身經過寫好的代碼,對命令作出反應(變化爲不一樣的state狀態)。github

React+Reflux起到的做用

如今你已經有了兩個小工具了,寫一個組件,經過Action調用組件就能夠了。
寫到這裏,你應該能體會到,全部的引入就是爲了讓代碼寫起來更有效率,更易用,複用性更強。web

Pure Component

純淨的組件:在給定相同props和state的狀況下會渲染出一樣結果
其優勢有這麼幾點:算法

  1. 咱們寫的組件都應該是隻依賴props和state的,而不該該依賴其餘全局變量或參數
  2. 純淨的組件方便複用、測試和維護

組件生命週期

React組件有兩部分

第一部分是初始化的生命週期:

  • getDefaultProps
  • geInitialState
  • componentWillMount
  • render
  • componentDidMount

第二部分是被action觸發,須要更新:
- shouldComponentUpdate
- componentWillUpdate
- render
- conponentDidUpdate

shouldComponentUpdate

shouldComponentUpdate這個方法能夠說是一個預留的插入接口。
在上面更新的時候,第一步就是調用的這個方法判斷組件是否該被從新渲染。

shouldComponentUpdate是在React組件更新的生命週期中,用於判斷組件是否須要從新渲染的一個接口,它有兩個返回值:
- 返回true,則進入React的Virtual DOM比較過程
- 返回false,則跳過Virtual DOM比較與渲染等過程

shouldComponentUpdate和Virtual DOM Equal compare

如上圖,這是一棵React Virtual DOM的樹。

  • C1在ShouldComponentUpdate返回了true,即默認值,表明須要更新,進入Virtual DOM Diff過程,返回false,不相同,須要更新
  • C2在ShouldComponentUpdate返回了false,再也不更新,C4,C5由於被父節點在ShouldComponentUpdate中返回了false,因此再也不更新
  • C3在ShouldComponentUpdate返回了true進入Virtual DOM Diff過程,比對結果爲false,新舊不同,須要更新
  • 輪到C6,ShouldComponentUpdate返回了true,進入Virtual DOM Diff的過程,返回了false,即新舊兩個節點不相同,因此這個節點須要更新
  • C7在ShouldComponentUpdate返回了false,即不須要更新,節點不變
  • C8在ShouldComponentUpdate返回了true,進入Virtual DOM Diff比對過程,結果爲true,新舊相等,不更新

大概就是這麼一個過程,在這裏,Diff算法其實仍是比較複雜的,比較好的作法是咱們來寫入ShouldComponentUpdate來本身控制組件的更新,而不是依賴React幫咱們作比較。

進入正文

前面講了那麼多,相信懂React的都懂了,就再也不詳細講了,Diff算法有興趣的能夠本身去翻源碼,網上也有一堆模擬實現的例子。

接下來介紹一個探索reflux&react渲染優化的例子。
這裏試圖,模擬一個比較現實的例子,拋開不少業務代碼,讓問題變得直接。

首先例子有三個組件,兩個按鈕,5個數字,還有一個重複打印文本的大組件。

  • 1basicDemo 是沒有優化的例子,每50ms會發出action更改store數據觸發渲染
  • 2perfDemo 使用addons插件Perf分析頁面性能的例子
  • 3pureRenderMixinDemo 使用addons插件pureRenderMixin優化頁面性能的例子
  • 4updateDemo 使用了addons插件update優化頁面性能的例子
  • 5immutableDemo 使用了Immutable.js優化頁面性能的例子

源代碼請點擊這裏

說明

  • gulpfile.js爲gulp構建代碼,會將tpl.js的JSX代碼翻譯爲js代碼,須要的能夠本身修改,每次轉化模板須要gulp運行一下
  • modulejs模塊加載器和myView單頁SPA框架爲騰訊通信與彩票業務部前端團隊這邊的基本框架,具體的請戳這裏查看
  • 須要關注的文件
    • index.html 頁面入口,規定了執行的模塊
    • app.js 應用程序入口
    • todoAction.js (reflux架構下,demo的action)
    • todoStore.js (reflux架構下,demo的store)
    • tpl.js 組件的jsx文件

簡單用法

  1. cd ./xxx/(這裏的xxx爲上面對應的 ……./4updateDemo/ 目錄)
  2. http-server -p 8888端口能夠自定義,http-server模塊已在node_module目錄下,擔憂版本依賴問題,已上傳node_module目錄,直接打開就能夠了
  3. 打開瀏覽器即可瀏覽,詳情請看控制檯

1.basicDemo

1basicDemo目錄是一個最原始的目錄,這裏你能夠看到咱們哪裏出現了問題。

cd ./example 打開這個沒優化過的例子的目錄
http-server -p xxxx 這裏端口隨意,不衝突就好
瀏覽器訪問並打開控制檯,會看到

5 tpl.js:32 createNum組件被更新了
  tpl.js:10 TextComponent被更新了
2 tpl.js:57 createBtn組件被更新了

初始化createNum組件被渲染了5次,由於有5個,createBtn組件被渲染了兩次,由於有點擊開始和點擊結束兩個按鈕。經過不一樣的傳參而改變形態。

點擊開始會觸發action,讓store的數據每次+1,點擊結束會清除定時器

點擊開始能夠看到控制檯的數據每次都會刷新整個界面的全部組件,特別是有一個大組件TextComponent,是重複5000次文本的,每次從新渲染就有不少的損耗。這就是咱們要優化的地方——減小某些關鍵部分的從新渲染的次數,減小無用對比的消耗

這裏你能夠打開Chrome控制檯的Timeline來看一下,點擊開始,打開Timeline面板,每1S左右會有一個腳本執行的高峯期。

咱們知道特別是在移動端,CPU和內存的資源顯得尤其稀缺(大概只能佔用正常CPU和內存的10%,微信手Q等可能會由於友商系統對應用程序的優先級設計使這個限制略有提升——我說的就是小米哈哈哈),因此這樣說來,性能這一塊在移動手機web顯得很是很是重要。

50ms渲染一次,重複渲染200次的截圖

2.Perl

Perl是react-addons帶來的性能分析工具,這裏的perfDemo是結合Chrome插件的例子。
要向全局暴露一個window.Perl變量,而後就能夠愉快的配合Chrome插件使用了

  • React-addons插件版本的Perf插件提供原生的API——用在首次渲染部分
  • Chrome插件——用在有交互的部分
  • console tool——須要查看對比新舊值的狀況下

這裏的wasted time就是在作屬性沒變化的重複渲染的過程,能夠優化。
用法與Chrome開發工具的TimeLine用法相似,點擊start開始記錄,後點擊stop結束

50ms渲染一次,重複渲染200次的截圖

3.PureRenderMixin

一個簡單的通用優化工具,經過淺對比(shallowCompare)方法對比新舊兩個組件的狀態,達到減小重複渲染的目的。

注意這裏組件的store必須無關聯,緣由是shallowCompare的時候,比較的是組件關聯的store的數據,而例子裏面store是一個,其餘組件num的變化也會引發這裏TextComponent組件的更新

這裏將store與頂級組件APP關聯起來,而後在子孫組件下自定採用props傳遞的方式處理(傳遞基本類型的數據),這樣就可讓pureRenderMixin的通用化了,惟一的缺點是,傳遞props要控制,只把組件須要的屬性傳遞下去,這裏會比較麻煩,可是這樣又是性能較高又比較好理解的處理方式(相對其餘要拷貝屬性的方式)

*store下,option裏面的對象,受pureRenderMixin的限制,不能夠出現引用類型

PureRenderMixin實際上是封裝了更底層的shallowCompare接口的

簡單用法以下:

var PureRenderMixin = require('react').addons.PureRenderMixin;
React.createClass({
  mixins: [PureRenderMixin],
  render: function() {
    return <div className={this.props.className}>foo</div>;
  }
});

就加了一個mixins,看起來簡單優雅有木有。能夠在衆多組件裏面copy通用啊有木有
那這裏幹了什麼?

React.addons = {
  CSSTransitionGroup: ReactCSSTransitionGroup,
  LinkedStateMixin: LinkedStateMixin,
  PureRenderMixin: ReactComponentWithPureRenderMixin, //看這裏
var ReactComponentWithPureRenderMixin = {
  //幫你寫了一個shouldComponentUpdate方法
  shouldComponentUpdate: function (nextProps, nextState) { 
    return shallowCompare(this, nextProps, nextState);
  }
};
function shallowCompare(instance, nextProps, nextState) {
  //分別比較props和state屬性是否相等
  return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
}
function shallowEqual(objA, objB) {
  if (objA === objB) { //store嵌套層級太深這裏就會返回true,引用類型內存指向同一空間
    return true;
  }

  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }

  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  var bHasOwnProperty = hasOwnProperty.bind(objB);
  for (var i = 0; i < keysA.length; i++) {
    if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
      return false;
    }
  }

  return true;
}

因此PureRenderMixin這個插件,只能比較state和props爲基本類型的部分。
若是有更加深層次的store數據嵌套,就要藉助於update插件或者Immutablejs來深拷貝store的數據另存一份了。

50ms渲染一次,重複渲染200次的截圖,引入pureRenderMixin

4.用update優化(也稱Immutable Helper)

update是addons裏面的一個方法,旨在對拷貝對象複雜的過程來作一些語法上的優化,具體能夠看react官方文檔

//extend複製對象屬性的時候
var newData = extend(myData, {
  x: extend(myData.x, {
    y: extend(myData.x.y, {z: 7}),
  }),
  a: extend(myData.a, {b: myData.a.b.concat(9)})
});
//用update的時候,提供了一些語法糖讓你不用寫那麼多
var update = require('react-addons-update');
var newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

cd ./updateDemo 打開這個用addons.update優化過的例子的目錄
http-server -p xxxx 這裏端口隨意,不衝突就好

這個例子與上面一個例子惟一的不一樣是這裏用了addons.update來進行store數據的複製,具體的能夠看todoStore和tpl這兩個模塊的代碼,其餘基本無修改

這裏update是參考了MongoDB’s query的部分語法,具體的能夠看這裏,類比數組方法,返回一個新的實例。

  • {$push: array} 相似數組的push方法
  • {$unshift: array} 相似數組的unshift方法
  • {$splice: array of arrays} 相似數組的splice方法
  • {$set: any} 整個替換目標
  • {$merge: object} 合併目標和object的 keys.
  • {$apply: function} 傳遞當前的值給 function 並用返回值更新它

可是由Timeline的觀察來看,複製對象屬性的性能遠比刷新一個大組件的性能高。

50ms渲染一次,重複渲染200次的截圖,引入了update模塊

5.Immutablejs

Immutable.js是Facebook爲解決數據持久化而獨立出來的一個庫,傳統的,好比咱們有

var a = {b:1};
function test(obj){
  obj.b = 10;
  return obj;
}
test(a); //10

函數對對象的操做,你不會知道這個函數對對象進行了什麼操做。也就是說是封閉的。
而Immutable每次對對象的操做都會返回一個新對象

Immutable.js提供了7種不可變的數據類型:List Map Stack OrderedMap Set OrderedSet Record,對Immutable對象的操做均會返回新的對象,例如:

var obj = {count: 1};
var map = Immutable.fromJS(obj);
var map2 = map.set('count', 2);

console.log(map.get('count')); // 1
console.log(map2.get('count')); // 2

引入Immutable.js,須要對現有的業務代碼進行改動,一般是對tpl和store兩部分進行操做,初始化數據的時候生成一個Immutable的數據類型,以後每次get,set操做都會返回一個共享的新的對象。

50ms渲染一次,重複渲染200次的截圖,引入了immutable用了其set方法:
50ms渲染一次,重複渲染200次的截圖,引入了immutable用了其set方法

50ms渲染一次,重複渲染200次的截圖,引入了immutable用了其update方法:
50ms渲染一次,重複渲染200次的截圖,引入了immutable用了其update方法

6.seamless-immutable && Observejs

一個是immutable的閹割版,一個是AlloyTeam推的。
二者都是經過Object.defineProperty(IE9+)對set和get操做進行處理,優勢是文件比較小。

7.寫在最後

本身設想,組件化運用到極致,應該是像微信weui那樣

  • 有一套很是適合接入,複用性很是強的組件庫。拿來就用,不須要再次開發
  • 應該兼顧起上面說的減小重複渲染的部分
  • 開發友好

這裏也思考一些可能作到的變化:

  • 將一個組件的action/store/JSX/樣式代碼Style 寫在一個文件裏,這樣方便修改和調用,封閉組件內部實現細節,對外只暴露action操做和store的一些get方法,這樣能夠修改或者是獲取到組件的某些如今時刻的屬性(也有同窗是直接封裝爲一個對象,經過對象暴露其store,action)
  • 組件共享或依賴的數據,應在公共父級的store或獨立成一個單獨的部分,而後採用props傳遞的形式或從獨立的store裏面取數據

License

源碼傳送門
MIT. Copyright (c) 2016 ManfredHu.

相關文章
相關標籤/搜索