深入淺出FE(六)React性能優化指南

1.使用純組件pureCommponent

如果 React 組件爲相同的狀態和 props 渲染相同的輸出,則可以將其視爲純組件。

對於像 this 的類組件來說,React 提供了 PureComponent 基類。擴展 React.PureComponent 類的類組件被視爲純組件。

 React.PureComponent 中以淺層對比 prop 和 state 的方式來實現了該函數。它與普通組件是一樣的,只是 PureComponents 負責 shouldComponentUpdate——它對狀態和 props 數據進行淺層比較(shallow comparison)。如果淺層比較相同則組件不會重新渲染。

缺點是可能會因深層的數據不一致而產生錯誤的否定判斷。

使用pureCommponent來實現淺比較,比較的是nextProps和nextState,根據最佳實踐,PureComponent不僅會影響本身,而且會影響子組件,所以PureComponent最佳情況是展示組件。

1、每次調用React組件都會重新創建組件,就算傳入的對象或者值沒有改變,他們的引用地址也會改變。style不要寫成這樣

<APP style={{color:'#000000'}} />

這樣每次渲染時style都是新對象,要將style提前賦值成常量,不要使用自變量

const defaultStyle = {};
<APP style={{this.props.style || defaultStyle}}/>

2、設置props並通過事件綁定在元素上

通過在constructor中設置綁定this,在render中直接使用this.xxx即可

3、設置子組件

當父組件中props變化時,子組件會發生不必要的渲染,可以給子組件設置PureComponent(即react-addons-pure-render-mixin)

,內部會實現shouldComponentUpdate方法,會進行一個淺比較,如果沒有變化就不會渲染。

什麼是淺層渲染?

在對比先前的 props 和狀態與下一個 props 和狀態時,淺層比較將檢查它們的基元是否有相同的值(例如:1 等於 1 或真等於真),還會檢查更復雜的 JavaScript 值(如對象和數組)之間的引用是否相同。

比較基元和對象引用的開銷比更新組件視圖要低。

因此,查找狀態和 props 值的變化會比不必要的更新更快。

2.使用 React.memo 進行組件記憶

react15.6引入的特性,默認情況下其只會對複雜對象做淺層對比。它會對組件做一個緩存,但是缺點是隻會對傳入的props做對比。如果你想要控制對比過程,那麼請將自定義的比較函數通過第二個參數傳入來實現。

3.使用shouldCommponentUpdate

這個原理也和pureComponent類似,主要就是利用nextprops和nextState防止重複渲染,可以在方法內部根據業務邏輯判斷當前的props和state來判斷組件是否需要更新。

4.reselector庫

可以處理將多個state轉化爲組件渲染所需要的數據做一些邏輯處理,同時對這些處理過程進行緩存,對較大計算函數有緩存作用。

5.connect函數第三第四個參數

第三個參數:mergeProps

第四個參數:自定義areStateEqual

6.recompose庫

目的是將組件力度轉化爲props粒度,根據每個prop變化來渲染組件

@pure props變化才渲染

@onlyUpdateForKeys(['props1, ''props2'])

7.不要在reder中執行與渲染無關的操作

即不要在render中執行計算等邏輯,或者執行一些業務處理,所有業務處理都應該在render之外,render只作爲渲染react組件的函數。

8.state和props結構要扁平化

如題所示

9.immutiblejs

JavaScript 中的對象一般是可變的(Mutable),因爲使用了引用賦值,新的對象簡單的引用了原始對象,改變新的對象將影響到原始對象。如 `foo={a: 1}; bar=foo; bar.a=2` 你會發現此時 `foo.a` 也被改成了 `2`。雖然這樣做可以節約內存,但當應用複雜後,這就造成了非常大的隱患,Mutable 帶來的優點變得得不償失。爲了解決這個問題,一般的做法是使用 shallowCopy(淺拷貝)或 deepCopy(深拷貝)來避免被修改,但這樣做造成了 CPU 和內存的浪費。

但對immutable對象修改、添加或者刪除操作,會返回一個新的inmutable對象,,原理是使用持久化的數據結構,也就是使用舊數據創建新數據時,要保證同時可用且不變。同時爲了避免深拷貝把所有節點都複製一遍帶來的性能損耗,Immutable使用了結構共享,即如果對象樹中一個節點發生變化,只修改這個節點和受他影響的父節點,其他節點則進行共享。

目前流行的 Immutable 庫有兩個:

1. immutable.js

Facebook 工程師 Lee Byron 花費 3 年時間打造,與 React 同期出現,但沒有被默認放到 React 工具集裏(React 提供了簡化的 Helper)。它內部實現了一套完整的 Persistent Data Structure,還有很多易用的數據類型。像 `Collection`、`List`、`Map`、`Set`、`Record`、`Seq`。有非常全面的`map`、`filter`、`groupBy`、`reduce``find`函數式操作方法。同時 API 也儘量與 Object 或 Array 類似。

其中有 3 種最重要的數據結構說明一下:(Java 程序員應該最熟悉了)

  • Map:鍵值對集合,對應於 Object,ES6 也有專門的 Map 對象
  • List:有序可重複的列表,對應於 Array
  • Set:無序且不可重複的列表

2. seamless-immutable

與 Immutable.js 學院派的風格不同,seamless-immutable 並沒有實現完整的 Persistent Data Structure,而是使用 `Object.defineProperty`(因此只能在 IE9 及以上使用)擴展了 JavaScript 的 Array 和 Object 對象來實現,只支持 Array 和 Object 兩種數據類型,API 基於與 Array 和 Object 操持不變。代碼庫非常小,壓縮後下載只有 2K。而 Immutable.js 壓縮後下載有 16K。

Immutable 優點

1. Immutable 降低了 Mutable 帶來的複雜度

可變(Mutable)數據耦合了 Time 和 Value 的概念,造成了數據很難被回溯。

比如下面一段代碼:

function touchAndLog(touchFn) {
  let data = { key: 'value' };
  touchFn(data);
  console.log(data.key); // 猜猜會打印什麼?
}

在不查看 `touchFn` 的代碼的情況下,因爲不確定它對 `data` 做了什麼,你是不可能知道會打印什麼(這不是廢話嗎)。但如果 `data` 是 Immutable 的呢,你可以很肯定的知道打印的是 `value`。

2. 節省內存

Immutable.js 使用了 Structure Sharing 會盡量複用內存。沒有被引用的對象會被垃圾回收。

import { Map} from 'immutable';
let a = Map({
  select: 'users',
  filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');

a === b; // false

a.get('filter') === b.get('filter'); // true

上面 a 和 b 共享了沒有變化的 `filter` 節點。

3. Undo/Redo,Copy/Paste,甚至時間旅行這些功能做起來小菜一碟

因爲每次數據都是不一樣的,只要把這些數據放到一個數組裏儲存起來,想回退到哪裏就拿出對應數據即可,很容易開發出撤銷重做這種功能。

後面我會提供 Flux 做 Undo 的示例。

4. 併發安全

傳統的併發非常難做,因爲要處理各種數據不一致問題,因此『聰明人』發明了各種鎖來解決。但使用了 Immutable 之後,數據天生是不可變的,併發鎖就不需要了。然而現在並沒什麼卵用,因爲 JavaScript 還是單線程運行的啊。但未來可能會加入,提前解決未來的問題不也挺好嗎?

5. 擁抱函數式編程

Immutable 本身就是函數式編程中的概念,純函數式編程比面向對象更適用於前端開發。因爲只要輸入一致,輸出必然一致,這樣開發的組件更易於調試和組裝。

像 ClojureScript,Elm 等函數式編程語言中的數據類型天生都是 Immutable 的,這也是爲什麼 ClojureScript 基於 React 的框架 --- Om 性能比 React 還要好的原因。

我們來代碼看看二者的不同

// 原來的寫法
let foo = {a: {b: 1}};
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b);  // 打印 2
console.log(foo === bar);  //  打印 true

// 使用 immutable.js 後
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b: 1}});
bar = foo.setIn(['a', 'b'], 2);   // 使用 setIn 賦值
console.log(foo.getIn(['a', 'b']));  // 使用 getIn 取值,打印 1
console.log(foo === bar);  //  打印 false

// 使用  seamless-immutable.js 後
import SImmutable from 'seamless-immutable';
foo = SImmutable({a: {b: 1}})
bar = foo.merge({a: { b: 2}})   // 使用 merge 賦值
console.log(foo.a.b);  // 像原生 Object 一樣取值,打印 1

console.log(foo === bar);  //  打印 false

使用 Immutable 的缺點

1. 需要學習新的 API

No Comments

2. 增加了資源文件大小

No Comments

3. 容易與原生對象混淆

這點是我們使用 Immutable.js 過程中遇到最大的問題。寫代碼要做思維上的轉變。

雖然 Immutable.js 儘量嘗試把 API 設計的原生對象類似,有的時候還是很難區別到底是 Immutable 對象還是原生對象,容易混淆操作。

Immutable 中的 Map 和 List 雖對應原生 Object 和 Array,但操作非常不同,比如你要用 `map.get('key')` 而不是 `map.key`,`array.get(0)` 而不是 `array[0]`。另外 Immutable 每次修改都會返回新對象,也很容易忘記賦值。

當使用外部庫的時候,一般需要使用原生對象,也很容易忘記轉換。

下面給出一些辦法來避免類似問題發生:

  • 使用 Flow 或 TypeScript 這類有靜態類型檢查的工具
  • 約定變量命名規則:如所有 Immutable 類型對象以 `$$` 開頭。
  • 使用 `Immutable.fromJS` 而不是 `Immutable.Map` 或 `Immutable.List` 來創建對象,這樣可以避免 Immutable 和原生對象間的混用。

9.使用唯一key

子組件使用唯一的key,關於使用key的介紹在這裏:https://reactjs.org/docs/lists-and-keys.html

10.使用hooks

通過hooks可以解決大型組件的維護及高階組件帶來的深層嵌套測試問題和一些複雜動畫問題,以及自定義hooks的使用。

還有useMemo(緩存值,類似React.momo)和useCallback(緩存函數)可以做緩存。

useMemo和useCallback的關係是:

useCallback(fn, deps) 相當於useMemo(() => fn, deps)

11.使用redux中間件

比如redux-saga、redux-thunk或者其他一些庫完成異步dispatch

12.在reducer使用web worker

通過中間件的形式使用web worker,用於改善在reducer中的計算量較大問題

13. 爲組件創建錯誤邊界

組件渲染錯誤是很常見的情況。

在這種情況下,組件錯誤不應該破壞整個應用。創建錯誤邊界可避免應用在特定組件發生錯誤時中斷。

錯誤邊界是一個 React 組件,可以捕獲子組件中的 JavaScript 錯誤。我們可以包含錯誤、記錄錯誤消息,併爲 UI 組件故障提供回退機制。

錯誤邊界是基於高階組件的概念。

詳細信息參閱: https://levelup.gitconnected.com/introduction-to-reacts-higher-order-components-hocs-c42182fb634

錯誤邊界涉及一個高階組件,包含以下方法:static getDerivedStateFromError() 和 componentDidCatch()。

static 函數用於指定回退機制,並從收到的錯誤中獲取組件的新狀態。

componentDidCatch 函數用來將錯誤信息記錄到應用中。

14. SSR

使用服務端渲染能能節省代碼量,有利於seo及首屏加載時間的優化。

15.事件節流和防抖

節流(throttling)和防抖(debouncing)可用來限制在指定時間內調用的事件處理程序的數量。

事件處理程序是響應不同事件(如鼠標單擊和頁面滾動)而調用的函數。事件觸發事件處理程序的速率是不一樣的。

節流的概念

節流意味着延遲函數執行。

這些函數不會立即執行,在觸發事件之前會加上幾毫秒延遲。

比如在頁面滾動時,我們不會過於頻繁地觸發滾動事件,而是將事件延遲一段時間以便將多個事件堆疊在一起。

它確保函數在特定時間段內至少調用一次。如果函數最近運行過了,它將阻止函數運行,確保函數以固定間隔定期運行。

當我們處理無限滾動並且當用戶接近頁面底部必須獲取數據時,我們可以使用節流。

16.使用 CDN

 CDN 是可在你的應用中使用的外部資源。我們甚至可以創建私有 CDN 並託管我們的文件和資源。

使用 CDN 有以下好處:

  • 不同的域名。瀏覽器限制了單個域名的併發連接數量,具體取決於瀏覽器設置。假設允許的併發連接數爲 10。如果要從單個域名中檢索 11 個資源,那麼同時完成的只有 10 個,還有 1 個需要再等一會兒。CDN 託管在不同的域名 / 服務器上。因此資源文件可以分佈在不同的域名中,提升了併發能力。

  • 文件可能已被緩存。有很多網站使用這些 CDN,因此你嘗試訪問的資源很可能已在瀏覽器中緩存好了。這時應用將訪問文件的已緩存版本,從而減少腳本和文件執行的網絡調用和延遲,提升應用性能。

  • 高容量基礎設施。這些 CDN 由大公司託管,因此可用的基礎設施非常龐大。他們的數據中心遍佈全球。向 CDN 發出請求時,它們將通過最近的數據中心提供服務,從而減少延遲。這些公司會對服務器做負載平衡,以確保請求到達最近的服務器並減少網絡延遲,提升應用性能。

如果擔心安全性,可以使用私有 CDN。

17. 用 CSS 動畫代替 JavaScript 動畫

在 HTML 5 和 CSS 3 出現之前,動畫曾經是 JavaScript 的專屬,但隨着 HTML 5 和 CSS 3 的引入情況開始變化。現在動畫甚至可以由 CSS 3 來處理了。

我們可以制定一些規則:

  • 如果 CSS 可以實現某些 JS 功能,那就用 CSS。

  • 如果 HTML 可以實現某些 JS 功能,那就用 HTML。

理由如下:

  1. 破損的 CSS 規則和樣式不會導致網頁損壞,而 JavaScript 則不然。

  2. 解析 CSS 是非常便宜的,因爲它是聲明性的。我們可以爲樣式並行創建內存中的表達,可以推遲樣式屬性的計算,直到元素繪製完成。

  3. 爲動畫加載 JavaScript 庫的成本相對較高,消耗更多網絡帶寬和計算時間。

  4. 雖然 JavaScript 可以提供比 CSS 更多的優化,但優化過的 JavaScript 代碼也可能卡住 UI 並導致 Web 瀏覽器崩潰。

詳細信息參閱: https://developers.google.com/web/fundamentals/design-and-ux/animations/css-vs-javascript

18. 在 Web 服務器上啓用 gzip 壓縮

壓縮是節省網絡帶寬和加速應用的最簡單方法。

我們可以把網絡資源壓縮到更小的尺寸。Gzip 是一種能夠快速壓縮和解壓縮文件的數據壓縮算法。

它可以壓縮幾乎所有類型的文件,例如圖像、文本、JavaScript 文件、樣式文件等。Gzip 減少了網頁需要傳輸到客戶端的數據量。

當 Web 服務器收到請求時,它會提取文件數據並查找 Accept-Encoding 標頭以確定如何壓縮應用。

如果服務器支持 gzip 壓縮,資源會被壓縮後通過網絡發送。每份資源的壓縮副本(添加了 Content-Encoding 標頭)指定使用 gzip 解壓。

然後,瀏覽器將內容解壓縮原始版本在渲染給用戶。

只是 gzip 壓縮需要付出成本,因爲壓縮和解壓縮文件屬於 CPU 密集型任務。但我們還是建議對網頁使用 gzip 壓縮。

詳細信息參閱: https://royal.pingdom.com/can-gzip-compression-really-improve-web-performance

19.Ramda 

是一個由許多高階函數組成、功能強大的函數庫。換句話說,就是許多用於創建函數的函數。由於我們的映射函數也不過只是函數而已,所以我們可以利用 Ramda 方便地創建 selectors。Ramda 可以完成所有 selectors 可以完成的工作,而且還不止於此。Ramda cookbook 中介紹了一些 Ramda 的應用示例。

具體可以參見阮一峯寫的這篇內容http://www.ruanyifeng.com/blog/2017/03/ramda.html

20.懶加載組件

導入多個文件合併到一個文件中的過程叫打包,使應用不必導入大量外部文件。

所有主要組件和外部依賴項都合併爲一個文件,通過網絡傳送出去以啓動並運行 Web 應用。

這樣可以節省大量網絡調用,但這個文件會變得很大,消耗大量網絡帶寬。

應用需要等待這個文件的加載和執行,所以傳輸延遲會帶來嚴重的影響。

爲了解決這個問題,我們引入代碼拆分的概念。

像 webpack 這樣的打包器支持就支持代碼拆分,它可以爲應用創建多個包,並在運行時動態加載,減少初始包的大小。

爲此我們使用 React的API ---Suspense 和 lazy這種新的api避免首次渲染頁面加載大量代碼塊,只引入我們需要的代碼塊。

或者使用Loadable等庫來實現懶加載。

參考書目:

1.《深入react技術棧》

2.《react狀態管理與同構實踐》

3.《react進階之路》

4.《react設計模式與最佳實踐》

 

可以加我vx:一起學習前端開發,一起成長。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章