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

優化后的
優化后的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM