React 效能最佳化,你需要知道的幾個點

joytoy發表於2021-09-09

寫了一段時間的react之後,漸漸的喜歡上了使用react來寫應用。

我們知道,Facebook在推出react時打出的旗號之一就是高效能。

今天我們還一起來聊一聊react的效能最佳化,思考還能透過哪些手段來提升React的效能,使我們的react更快,效能更好。

一,react元件的效能最佳化(渲染角度最佳化)

1,react效能檢視工具

再講效能最佳化之前,我們需要先來了解一下如何檢視react載入元件時所耗費的時間的工具,在react 16版本之前我們可以使用React Perf來檢視。

大家可以在chorme中先安裝React Perf擴充套件,然後在入口檔案或者reduxstore.js中加入相應的程式碼即可:

在最新的React16版本中,我們可以直接在url後加上?react_pref,就可以在chrome瀏覽器的performance,我們可以檢視User Timeing來檢視元件的載入時間。

圖片描述

react16.0_pref


使用此工具的具體操作大家可以看下圖:


圖片描述

react16.0_pref.gif


2,單個react元件效能最佳化

2.1,render裡面儘量減少新建變數和bind函式,傳遞引數是儘量減少傳遞引數的數量。

首先我們先思考一個問題,比如我要實現一個點選按鈕使相應的num增加1,我們有哪一些方法。

大家應該都能想到,無非就是三種,如下圖:

圖片描述

react_function

第一種是在建構函式中繫結this,第二種是在render()函式里面繫結this,第三種就是使用箭頭函式,都能實現上述方法;

但是哪一種方法的效能最好,是我們要考慮的問題。應該大家都知道答案:第一種的效能最好

因為第一種,建構函式每一次渲染的時候只會執行一遍;

而第二種方法,在每次render()的時候都會重新執行一遍函式;

第三種方法的話,每一次render()的時候,都會生成一個新的箭頭函式,即使兩個箭頭函式的內容是一樣的。

第三種方法我們可以舉一個例子,因為react判斷是否需要進行render淺層比較,簡單來說就是透過===來判斷的,如果state或者prop的型別是字串或者數字,只要值相同,那麼淺層比較就會認為其相同;

但是如果前者的型別是複雜的物件的時候,我們知道物件是引用型別,淺層比較只會認為這兩個prop是不是同一個引用,如果不是,哪怕這兩個物件中的內容完全一樣,也會被認為是兩個不同的prop

舉個例子:

當我們給元件Foo給名為styleprop賦值;

使用這種方法,每一次渲染都會被認為是一個style這個prop發生了變化,因為每一次都會產生一個物件給style

那麼我們應該如何改進,如果想要讓react渲染的時候認為前後物件型別prop相同,則必須要保證prop指向同一個javascript物件,如下:

const fooStyle = { color: "red" }; //取保這個初始化只執行一次,不要放在render中,可以放在建構函式中

這個問題是我們在平時的編碼中可以避免的。


2.2,定製shouldComponentUpdate函式

shouldComponentUpdate是決定react元件什麼時候能夠不重新渲染的函式,但是這個函式預設的實現方式就是簡單的返回一個true。也就是說,預設每次更新的時候都要呼叫所用的生命週期函式,包括render函式,重新渲染。

我們來看一下下面的一個例子

圖片描述

shouldComponentUpdate

我們寫兩個元件,AppDemo元件,並寫兩個方法,一個改變App中的num的值,一個是改變title,我們在Demo的render中列印render函式。我們可以看到以下的效果:

我們可以清晰的看到雖然demo元件裡的title值沒有改變,但是還是render了。

為了解決這個問題,我們可以對demo元件進行如下的修改:

只有當demo的title值發生改變的時候,我們才去render,我們可以看一下效果:

以上只是一個特別簡單的一個對於shouldComponentUpdate的定製。

在最新的react中,react給我們提供了React.PureComponent,官方也在早期提供了名為react-addons-pure-render-mixin外掛來重新實現shouldComponentUpdate生命週期方法。


透過上述的方法的效果也是和我們定製shouldComponentUpdate的效果是一致的。

但是我們要注意的是,這裡的PureRender是淺比較的,因為深比較的場景是相當昂貴的。所以我們要注意我們在1.1中說到的一些注意點:不要直接為props設定物件或者陣列不要將方法直接繫結在元素上,因為其實函式也是物件


2.3,Immutable.js


Shared mutable state is the root of all evil(共享的可變狀態是萬惡之源)

-- Pete Hunt

javascript中的物件一般都是可變的,因為使用了引用賦值,新的物件簡單的引用了原始物件,改變新物件將影響到原始物件。

舉個例子:

foo = { a : 1 };
bar = foo;
bar.a = 2;

當我們給bar.a賦值後,會發現foo.a也變成了2,雖然我們可以透過深複製與淺複製解決這個問題,但是這樣做非常的昂貴,對cpu和記憶體會造成浪費。

這裡就需要用到Immutable,透過Immutable建立的Immutable Data一旦被建立,就不能再更改。對Immutable物件進行修改、新增或刪除操作,都會返回一個新的Immutable物件。

這裡我們將一下其中三個比較重要的資料結構

  • Map:鍵值對集合,對應ObjectEs6種也有專門的Map物件

  • List:有序可重複列表,對應於Array

  • ArraySet:有序且不可重複的列表

我們可以看兩個例子:

使用Map生成一個immutable物件

import { Map , is } from 'immutable';let obj = Map({  'name': 'react study',  'course': Map({name: 'react+redux'})
})let obj1 = obj.set('name','darrell');console.log(obj.get('course') === obj1.get('course')); // 返回trueconsole.log(obj === obj1); // 返回false

Immutable.is 比較的是兩個物件的 hashCodevalueOf(對於 JavaScript 物件)。由於 immutable 內部使用了 Trie 資料結構來儲存,只要兩個物件的 hashCode 相等,值就是一樣的。這樣的演算法避免了深度遍歷比較,效能非常好。

let obj = Map({name:1,title:'react'});let obj1 = Map({name:1,title:'react'});console.log(is(obj,obj1)); // 返回truelet obj2 = {name:1,title:'react'};let obj3 = {name:1,title:'react'};console.log(is(obj2,obj3)); // 返回false

Immutable優點

  • 減少記憶體的使用

  • 併發安全

  • 降低專案的複雜度

  • 便於比較複雜資料,定製shouldComponentUpdate方便

  • 時間旅行功能

  • 函數語言程式設計

Immutable缺點

  • 學習成本

  • 庫的大小(建議使用seamless-immutable)

  • 對現有專案入侵嚴重

  • 容易與原生的物件進行混淆

如果大家想深入瞭解,可以參考、。


2.4,多個react元件效能最佳化,key的最佳化

react元件在裝載過程中,react透過在render方法在記憶體中產生一個樹形結構,樹上的節點代表一個react元件或者原生的Dom元素,這個樹形結構就是我們所謂的Vitural Dom,react根據這個來渲染產生瀏覽器的Dom樹。

react在更新階段對比原有的Vitural Dom和新生成的Vitural Dom,找出不同之處,在根據不同來渲染Dom樹。

react為了追求高效能,採用了時間複雜度為O(N)來比較兩個屬性結構的區別,因為要確切比較兩個樹形結構,需要透過O(N^3),這會降低效能。

我們舉幾個情況,大家就會馬上理解:

  • 節點型別不同

    // A元件
      
    // B元件  

    我們想把A元件更新成B元件,react在做比較的時候,發現最外面的根結點不一樣,直接就廢掉了之前的

    節點,包括裡面的子節點,這是一個巨大的浪費,但是為了避免O(N^3)的時間複雜度,只能採用這種方式

    所以在開發過程中,我們應該儘量避免上面的情況,不要將包裹節點的型別隨意改變。

  • 兩個節點型別一樣

    這裡包括兩種情況,一種是節點是Dom型別,還有一種react元件。

    對於dom型別,我們舉個例子:

    // A元件
      Hello World!!!
    // B元件
      Good Bye!!!

    上述A和B元件的區別是文字、classNamestyle中的color發生改變,因為Dom元素沒變,React只會修改他變化的部分。

    針對react元件型別,渲染無非就是在走一遍元件例項的更新過程,最主要的就是定製shouldComponentUpdate,我們上面也有講到,就不細講了。

  • 多個子元件情況

    我們看兩個例子就能明白

    例子一:

    // A
           
    // B
              

    從A變到B,如果shouldComponentUpdate處理得當,我們只需要更新裝載third的那一次就行。

    我們來看看下一個例子:

    // A
           
    // B
              

    這裡因為react是採用O(n)的時間複雜度,所以會依次將text為First的改為Zero,text為Second改為First,在最後再加上一個元件,text為Second。現存的兩個的text的屬性都被改變了,所以會依次渲染。

    如果我們這裡有1000個例項,那麼就會發生1000次更新。

    這裡我們就要用到Key

    簡單來說,其實這一個Key就是react元件的身份證號。

    我們將上一個例子改成如下,就可以避免上面的問題了,react就能夠知道其實B裡面的第二個和第三個元件其實就是A中的第一個和第二個例項。

    // A
           
    // B
              

    不過現在,react也會提醒我們不要忘記使用key,如果沒有加,在瀏覽器中會報錯。



    關於key的使用我們要注意的是,這個key值要穩定不變的,就如同身份證號之於我們是穩定不變的一樣。

    一個常見的錯誤就是,拿陣列的的下標值去當做key,這個是很危險的,程式碼如下,我們一定要避免。

    
    
        {         todos.map((item, index) => {             



  • 二,redux效能最佳化:reselect(資料獲取時最佳化)

    在前面的最佳化過程中,我們都是最佳化渲染來提高效能的,既然reactredux都是透過資料驅動的的方式驅動渲染過程,那麼處理最佳化渲染過程,獲取資料的過程也是需要考慮的一個最佳化點。

    //下面是redux中簡單的一個篩選功能const getVisibleTodos = (todos, filter) => {  switch (filter) {    case 'SHOW_ALL':      return todos    case 'SHOW_COMPLETED':      return todos.filter(t => t.completed)    case 'SHOW_ACTIVE':      return todos.filter(t => !t.completed)
      }
    }const mapStateToProps = (state) => {  return {    todos: getVisibleTodos(state.todos, state.visibilityFilter)
      }
    }

    mapStateToProps函式作為redux store中獲取資料的重要一環,當我們根據filtertodos來顯示相應的待辦事項的時候,我們都要遍歷todos欄位上的陣列。

    當陣列比較大的時候,則會降低效能。

    這個時候,reselect就應運而生了,它的動作原理:只要相關的狀態沒有改變,那麼就直接使用上一次的快取結果。

    具體的用法我就不在這裡過多介紹了,已經有很多的牛人寫了相關的文章,我也不重複寫了,大家如果想深入瞭解的話,可以參考、。


    三:參考資料

    此篇文章是參考了《深入React技術棧》和《深入淺出React與Redux》這兩本書中關於對react效能最佳化的章節,再加上自己的動手實踐與思考寫的。

    文章中不乏會有些錯誤的地方還請大家多多批評指正。



    作者:darrell
    連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4650/viewspace-2802968/,如需轉載,請註明出處,否則將追究法律責任。

相關文章