React 效能優化

開水泡飯的部落格 發表於 2021-12-02
React

react 效能優化

React 元件效能優化的核心就是減少渲染真實DOM節點的頻率,減少Virtual DOM 對比的頻率,以此來提高效能

1. 元件解除安裝之前進行清理操作

在元件中為window 註冊的全域性事件,以及定時器,在元件解除安裝前要清理掉,防止元件解除安裝後繼續執行影響應用效能

我們開啟一個定時器然後解除安裝元件,檢視元件中的定時器是否還在執行 Test 元件來開啟一個定時器

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    setInterval(() => {
      console.log('定時器開始執行')
    }, 1000)
  }, [])
  return <div>Test</div>
}

在App.js中引入定時器元件然後用flag變數來控制渲染和解除安裝元件

import Test from "./Test";
import { useState } from "react"
function App() {
  const [flag, setFlag] = useState(true)
  return (
    <div>
      { flag && <Test /> }
      <button onClick={() => setFlag(prev => !prev)}>點選按鈕</button>
    </div>
  );
}

export default App;

在瀏覽器中我們去點選按鈕發現元件被解除安裝後定時器還在執行,這樣元件太多之後或者這個元件不停的渲染和解除安裝會開啟很多的定時器,我們應用的效能肯定會被拉垮,所以我們需要在組建解除安裝的時候去銷燬定時器。

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    // 因為要銷燬定時器所以我們需要用一個變數來接受定時器id
    const InterValTemp =  setInterval(() => {
      console.log('定時器開始執行')
    }, 1000)
    return () => {
      console.log(`ID為${InterValTemp}定時器被銷燬了`)
      clearInterval(InterValTemp)
    }
  }, [])
  return <div>Test</div>
}

這個時候我們在去點選銷燬組建的時候定時器就被銷燬掉了

2. 類元件用純元件來提升組建效能PureComponent

1. 什麼是純元件

​ 純元件會對組建的輸入資料進行淺層比較,如果輸入資料和上次輸入資料相同,組建不會被重新渲染

2. 什麼是淺層比較

​ 比較引用資料型別在記憶體中的引用地址是否相同,比較基本資料型別的值是否相同

3. 如何實現純元件

​ 類元件整合 PureComponent 類,函式元件使用memo方法

4. 為什麼不直接進行diff操作,而是要進行淺層比較,淺層比較難到沒有效能消耗嗎

​ 和進行 diff 比較操作相比,淺層比較小號更少的效能,diff 操作會重新遍歷整個 virtualDOM 樹,而淺層比較只比較操作當前元件的 state和props

在狀態中儲存一個name為張三的,在組建掛載後我們每隔1秒更改name的值為張三,然後我們看純元件和非純元件,檢視結果

// 純元件
import { PureComponent } from 'react'
class PureComponentDemo extends PureComponent {
  render () {
    console.log("純元件")
    return <div>{this.props.name}</div>
  }
}
// 非純元件
import { Component } from 'react'
class ReguarComponent extends Component {
 render () {
   console.log("非純元件")
   return <div>{this.props.name}</div>
 }
}

引入純元件和非純元件 並在元件掛在後開啟定時器每隔1秒更改name的值為張三

import { Component } from 'react'
import { ReguarComponent, PureComponentDemo } from './PureComponent'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
  }
  updateName () {
    setInterval(() => {
      this.setState({name: "張三"})
    }, 1000)
  }
  componentDidMount () {
    this.updateName()
  }
  render () {
    return <div>
      <ReguarComponent name={this.state.name}></ReguarComponent>
      <PureComponentDemo name={this.state.name}></PureComponentDemo>
    </div>
  }
}

開啟瀏覽器檢視執行結果

image-20210922214700974

我們發現純元件只執行了一次,以後在改相同的值的時候,並沒有再重新渲染元件,而非純元件則是每次更改都在重新渲染,所以純元件要比非純元件更節約效能

3. 函式元件來實現純元件 memo

  1. memo 基本使用

    將函式元件變成純元件,將當前的props和上一次的props進行淺層比較,如果相同就元件元件的渲染。》。

我們在父元件中維護兩個狀態,index和name 開啟定時器讓index不斷地發生變化,name傳遞給子元件,檢視父元件更新子元件是否也更新了, 我們先不用memo來檢視結果

import { useState, useEffect } from 'react'
function App () {
  const [ name ] = useState("張三")
  const [index, setIndex] = useState(0)

  useEffect(() => {
    setInterval (() => {
      setIndex(prev => prev + 1)
    }, 1000)
  }, [])

  return <div>
    {index}
    <ShowName name={name}></ShowName>
  </div>
}

function ShowName ({name}) {
  console.log("元件被更新")
  return <div>{name}</div>
}

開啟瀏覽器檢視執行結果

image-20210923231543043

在不使用 memo 來把函式元件變成純元件的情況下我們發現子元件隨著父元件更新而一起重新渲染,但是它依賴的值並沒有更新,這樣浪費了效能,我們使用 memo 來避免沒必要的更新

import { useState, useEffect, memo } from 'react'

const ShowName = memo(function ShowName ({name}) {
  console.log("元件被更新")
  return <div>{name}</div>
})

function App () {
  const [ name ] = useState("張三")
  const [index, setIndex] = useState(0)

  useEffect(() => {
    setInterval (() => {
      setIndex(prev => prev + 1)
    }, 1000)
  }, [])

  return <div>
    {index}
    <ShowName name={name}></ShowName>
  </div>
}

我們再次開啟瀏覽器檢視執行結果

image-20210922222640420

現在index變動 子元件沒有重新渲染了,用 memo 把元件變為純元件之後就避免了依賴的值沒有更新卻跟著父元件一起更新的情況

4. 函式元件來實現純元件(為memo方法傳遞自定義比較邏輯)

memo 方法也是淺層比較

memo 方法是有第二個引數的第二個引數是一個函式

這個函式有個兩個引數,第一個引數是上一次的props,第二個引數是下一個props

這個函式返回 false 代表重新渲染, 返回true 重新渲染

比如我們有員工姓名和職位兩個資料,但是頁面中只使用了員工姓名,那我們只需要觀察員工姓名發生變動沒有,所以我們在memo的第二個引數去比較是否需要重新渲染

import { useState, useEffect, memo } from 'react'

function compare (prevProps, nextProps) {
  if (prevProps.person.name !== nextProps.person.name) {
    return false
  }
  return true
}

const ShowName = memo(function ShowName ({person}) {
  console.log("元件被更新")
  return <div>{person.name}</div>
}, compare)

function App () {
  const [ person, setPerson ] = useState({ name: "張三", job: "工程師"})

  useEffect(() => {
    setInterval (() => {
      setPerson({
        ...person,
        job: "挑糞"
      })
    }, 1000)
  }, [person])

  return <div>
    <ShowName person={person}></ShowName>
  </div>
}

5. shouldComponentUpdata

純元件只能進行淺層比較,要進行深層次比較,使用 shouldComponentUpdate,它用於編寫自定義比較邏輯

返回true 重新渲染元件, 返回 false 元件重新渲染元件

函式的第一個引數為 nextProps,第二個引數為NextState

比如我們有員工姓名和職位兩個資料,但是頁面中只使用了員工姓名,那我們只需要觀察員工姓名發生變動沒有,利用shouldComponentUpdata來控制只有員工姓名發生變動才重新渲染元件,我們檢視使用 shouldComponentUpdata 生命週期函式和不使用shouldComponentUpdata生命週期函式的區別

// 沒有使用的元件
import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: '張三',
        job: '工程師'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "修水管"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render 方法執行了")
    return <div>
      {this.state.person.name}
    </div>
  }
}

我們開啟瀏覽器等待兩秒

image-20210922220251277

發現render方法執行了兩次,元件被重新渲染了,但是我們並沒有更改name 屬性,所以這樣浪費了效能,我們用shouldComponentUpdata生命週期函式來判斷name是否發生了改變

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: '張三',
        job: '工程師'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "修水管"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render 方法執行了")
    return <div>
      {this.state.person.name}
    </div>
  }
  shouldComponentUpdate (nextProps, nextState) {
    if (this.state.person.name !== nextState.person.name) {
      return true;
    }
    return false;
  }
}

我們再開啟瀏覽器等待兩秒之後

image-20210922220711461

我們只改變了job 的時候render方法只執行了一次,這樣就減少了沒有必要的渲染,從而節約了效能

6. 使用元件懶載入

使用路由懶載入可以減少bundle檔案大小,從而加快組建呈遞速度

建立 Home 組建

// Home.js
function Home() {
  return (
    <div>
      首頁
    </div>
  )
}

export default Home

建立 List 組建

// List.js
function List() {
  return (
    <div>
      列表頁
    </div>
  )
}

export default List

從react-router-dom包中引入 BrowserRouter, Route, Switch, Link 和 home 與list 來建立路由規則以及切換區域和跳轉按鈕

import { BrowserRouter, Route, Switch, Link } from 'react-router-dom'
import Home from './Home';
import List from './List';

function App () {
  return <div>
    <BrowserRouter>
        <Link to="/">首頁</Link>
        <Link to="/list">列表頁</Link>
      <Switch>
          <Route path="/" exact component={Home}></Route>
          <Route path="/list" component={List}></Route>
      </Switch>
    </BrowserRouter>
  </div>
}

使用 lazy, Suspense 來建立載入區域與載入函式

import { lazy, Suspense } from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom'

const Home = lazy(() => import('./Home'))
const List = lazy(() => import('./List'))

function Loading () {
  return <div>loading</div>
}

function App () {
  return <div>
    <BrowserRouter>
        <Link to="/">首頁</Link>
        <Link to="/list">列表頁</Link>
      <Switch>
        <Suspense fallback={<Loading />}>
          <Route path="/" exact component={Home}></Route>
          <Route path="/list" component={List}></Route>
        </Suspense>
      </Switch>
    </BrowserRouter>
  </div>
}

使用註解方式來為打包後的檔案命名

const Home = lazy(() => import(/* webpackChunkName: "Home"  */'./Home'))
const List = lazy(() => import(/* webpackChunkName: "List" */'./List'))

7. 根據條件進行元件懶載入

適用於元件不會隨條件頻繁切換

import { lazy, Suspense } from 'react';


function App () {
  let LazyComponent = null;
  if (false){
    LazyComponent = lazy(() => import(/* webpackChunkName: "Home"  */'./Home'))
  } else {
    LazyComponent = lazy(() => import(/* webpackChunkName: "List" */'./List'))
  }
  return <div>
    <Suspense fallback={<div>loading</div>}>
      <LazyComponent />
    </Suspense>
  </div>
}

export default App;

這樣就只會載入一個元件從而提升效能

8. 通過使用佔位符標記提升React元件的渲染效能

React元件中返回的jsx如果有多個同級元素必須要有一個共同的父級

function App () {
  return (<div>
    	<div>1</div>
      <div>2</div>
    </div>)
}

為了滿足這個條件我們通常會在外面加一個div,但是這樣的話就會多出一個無意義的標記,如果每個元素都多處這樣的一個無意義標記的話,瀏覽器渲染引擎的負擔就會加劇

為了解決這個問題,React 推出了 fragment 佔位符標記,使用佔位符編輯既滿足了共同父級的要求,也不會渲染一個無意義的標記

import { Fragment } from 'react'
function App () {
  return <Fragment>
  		<div>1</div>
    	<div>1</div>
  </Fragment>
}

當然 fragment 標記還是太長了,所以有還有簡寫方法

function App () {
  return <>
  		<div>1</div>
    	<div>1</div>
  </>
}

9. 不要使用行內函數定義

在使用行內函數後,render 方法每次執行後都會建立該函式的新例項,導致 React 在進行 Virtual DOM 對比的時候,新舊函式比對不相等,導致 React 總是為元素繫結新的函式例項,而舊的函式有要交給垃圾回收器處

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={() => { this.setState({name: "李四"})}}>修改</button>
    </div>
  }
}


export default App;

修改為以下的方式

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

10. 在建構函式中進行函式this繫結

在類元件中如果使用 fn(){} 這種方式定義函式,函式的 this 指向預設只想 undefined,也就是說函式內部的 this 指向需要被更正,

可以在建構函式中對函式進行 this 更正,也可以在內部進行更正,兩者看起來沒有太大差別,但是對效能影響是不同的

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
    // 這種方式應為構造器只會執行一次所以只會執行一次
    this.setChangeName = this.setChangeName.bind(this)
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* 這種方式在render方法執行的時候就會生成新的函式例項 */}
      <button onClick={this.setChangeName.bind(this)}>修改</button>
    </div>
  }
  setChangeName() {
    this.setState({name: "李四"})
  }
}

在建構函式中更正this指向只會更正一次,而在render方法中如果不更正this指向的話 那麼就是 undefined ,但是在render方法中更正的話render方法的每次執行都會返回新的函式例項這樣是對效能是有所影響的

11. 類元件中的箭頭函式

在類元件中使用箭頭函式不會存在this指向問題,因為箭頭函式不繫結this

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* <button onClick={() => { this.setState({name: "李四"})}}>修改</button> */}
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

箭頭函式在this指向上確實比較有優勢

但是箭頭函式在類元件中作為成員使用的時候,該函式會被新增成例項物件屬性,而不是原型物件屬性,如果元件被多次重用,每個元件例項都會有一個相同的函式例項,降低了函式例項的可用性造成了資源浪費

綜上所述,我們得出結論,在使用類元件的時候還是推薦在建構函式中通過使用bind方法更正this指向問題

12. 避免使用內聯樣式屬性

當使用內聯樣式的時候,內聯樣式會被編譯成JavaScript程式碼,通過javascript程式碼將樣式規則對映到元素身上,瀏覽器就會畫更多的時間執行指令碼和渲染UI,從而增加了元件的渲染時間

function App () {
  return <div style={{backgroundColor: 'red';}}></div>
}

在上面的元件中,為元素增加了背景顏色為紅色,這個樣式為JavaScript物件,背景顏色需要被轉換成等效的css規則,然後應用到元素上,這樣涉及了指令碼的執行,實際上內聯樣式的問題在於是在執行的時候為元素新增樣式,而不是在編譯的時候為元素新增樣式

更好的方式是匯入樣式檔案,能通過css直接做的事情就不要通過JavaScript來做,因為JavaScript操作 DOM 非常慢

13. 優化條件渲染以提升元件效能

頻繁的掛在和解除安裝元件是一件非常耗效能的事情,應該減少元件的掛載和解除安裝次數,

在React中 我們經常會通過不同的條件渲染不同的元件,條件渲染是一必須做的優化操作.

function App () {
  if (true) {
    return <div>
      <Component1 />
    	<Component2 />
      <Component3 />
  	</div>
  } else {
    return <div>
        <Component2 />
      	<Component3 />
    </div>
  }
  
}

上面的程式碼中條件不同的時候,React 內部在進行Virtual DOM 對比的時候發現第一個元素和第二個元素都已經發生變化,所以會解除安裝元件1、元件2、元件3,然後再渲染元件2、元件3。實際上變化的只有元件1,重新掛在元件2和元件3時沒有必要的

function App () {
  if (true) {
    return <div>
      { true && <Component1 />}
    	<Component2 />
      <Component3 />
  	</div>
  }
}

這樣變化的就只有元件1了節省了不必要的渲染

16. 避免重複的無限渲染

當應用程式狀態更改的時候,React 會呼叫 render方法 如果在render方法中繼續更改應用程式狀態,就會發生遞迴呼叫導致應用報錯

image-20210923220549762

未捕獲錯誤:超出最大更新深度。當元件在componentWillUpdate或componentDidUpdate內重複呼叫setState時,可能會發生這種情況。React限制巢狀更新的數量以防止無限迴圈。React限制的最大次數為50次

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '張三'
    }
  }
  render () {
    this.setState({name:"張五"})
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

與其他生命週期函式不同,render 方法應該被作為純函式,這意味著,在render方法中不要做以下事情

  1. 不要呼叫 setState 方法去更改狀態、
  2. 不要使用其他手段查詢更改 DOM 元素,以及其他更改應用程式的操作、
  3. 不要在componentWillUpdate生命週期中重複呼叫setState方法更改狀態、
  4. 不要在componentDidUpdate生命週期中重複呼叫setState方法更改狀態、

render方法執行根據狀態改變執行,這樣可以保持元件的行為與渲染方式一致

15. 為元件建立錯誤邊界

預設情況下,元件渲染錯誤會導致整個應用程式中斷,建立錯誤邊界可以確保元件在發生錯誤的時候應用程式不會中斷,錯誤邊界是一個React元件,可以捕獲子級元件在渲染是發生錯誤,當錯誤發生時,可以記錄下來,可以顯示備用UI介面,

錯誤邊界涉及到兩個生命週期,分別是 getDerivedStateFromError 和 componentDidCatch.

getDerivedStateFromError 為靜態方法,方法中需要返回一個物件,該物件會和state物件進行合併,用於更改應用程式狀態.

componentDidCatch 方法用於記錄應用程式錯誤資訊,該方法返回的是錯誤物件

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      hasError: false
    }
  }
  componentDidCatch (error) {
    console.log(error)
  }
  static getDerivedStateFromError () {
    return {
      hasError: true
    }
  }
  render () {
    if (this.state.hanError) {
      return <div>
        發生錯誤了
      </div>
    }
    return <Test></Test>
  }
}

class Test extends Component {
  constructor () {
    super()
    this.state = {
      hanError: false
    }
  }
  render () {
    throw new Error("發生了錯誤");
    return <div>
      正確的
    </div>
  }
}

當我們丟擲錯誤的時候,getDerivedStateFromError 會合並返回的物件到state 所以hasError會變成true 就會渲染我們備用的介面了

注意: getDerivedStateFromError 不能捕獲非同步錯誤,譬如按鈕點選事件發生後的錯誤

16. 避免資料結構突變

元件中 props 和 state 的資料結構應該保持一致,資料結構突變會導致輸出不一致

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "張三",
        age: 18
      }
    }
    this.setMan = this.setMan.bind(this)
  }
  render () {
    const { name, age } = this.state.man
    return <div>
      <p>
        {name}
        {age}
      </p>
      <button onClick={this.setMan}>修改</button>
    </div>
  }
  setMan () {
    this.setState({
      ...this.state,
      man: {
        name: "李四"
      }
    })
  }
}

乍一看這個程式碼貌似沒有問題,仔細一看我們發現,在我們修改了名字之後年齡欄位丟失了,因為資料突變了 ,我們應該去避免這樣的資料突變

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "張三",
        age: 18
      }
    }
    this.setMan = this.setMan.bind(this)
  }
  render () {
    const { name, age } = this.state.man
    return <div>
      <p>
        {name}
        {age}
      </p>
      <button onClick={this.setMan}>修改</button>
    </div>
  }
  setMan () {
    this.setState({
      man: {
        ...this.state.man,
        name: "李四"
      }
    })
  }
}

17. 依賴優化

在應用程式中我們經常使用地三方的包,但我們不想引用包中的所有程式碼,我們只想用到那些程式碼就包含那些程式碼,此時我們可以使用外掛對依賴項進行優化

我們使用 lodash 舉例子. 應用基於 create-react-app 腳手架建立

1. 下載依賴

npm install react-app-rewired customize-cra lodash babel-plugin-lodash

react-app-rewired: 覆蓋create-react-app 配置

module.exports = function (oldConfig) {
  	return	newConfig
}

customize-cra: 匯出輔助方法,可以讓以上寫法更簡潔

const { override, useBabelRc } = require("customize-cra")
module.exports = override(
	(oldConfig) => newConfig,	
  (oldConfig) => newConfig,
)

override: 可以接收多個引數,每個引數都是一個配置函式,函式接受oldConfig,返回newConfig

useBabelRc:允許使用.babelrc 檔案進行babel 配置

babel-plugin-lodash:對lodash 進行精簡

2. 在專案的根目錄新建 config-overrides.js 並加入以下配置

const { override, useBabelRc } = require("customize-cra")

module.exports = override(useBabelRc())

3. 修改package.json檔案中的構建命令

{
  "script": {
       "start": "react-app-rewired start",
       "build": "react-app-rewired build",
       "test": "react-app-rewired test --env=jsdom",
       "eject": "react-scripts eject"
  }
}

4. 建立 .babelrc 檔案並加入配置

{
  "plugins": ["lodash"]
}

5. 生產環境下的三種 JS檔案

  1. main.[hash].chunk.js:這是你的應用程式程式碼,App.js 等.
  2. 1.[hash].chunk.js:這是第三方庫的程式碼,包含你在 node_modules 中匯入的模組.
  3. runtime~main.[hash].js:webpack 執行時程式碼.

6. App 元件中程式碼

import _ from 'lodash'

function App () {
   console.log(_.chunk(['a', 'b', 'c', 'd']))
  return	<div>Test</div>
}

沒有引入lodash
沒有引入lodash

引入lodash
引入lodash

優化後的
優化後的