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>
}
}
打開瀏覽器查看執行結果

我們發現純組件只執行了一次,以后在改相同的值的時候,并沒有再重新渲染組件,而非純組件則是每次更改都在重新渲染,所以純組件要比非純組件更節約性能
3. 函陣列件來實作純組件 memo
-
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>
}
打開瀏覽器查看執行結果

在不使用 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>
}
我們再次打開瀏覽器查看執行結果

現在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>
}
}
我們打開瀏覽器等待兩秒

發現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;
}
}
我們再打開瀏覽器等待兩秒之后

我們只改變了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方法中繼續更改應用程式狀態,就會發生遞回呼叫導致應用報錯

未捕獲錯誤:超出最大更新深度,當組件在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方法中不要做以下事情
- 不要呼叫 setState 方法去更改狀態、
- 不要使用其他手段查詢更改 DOM 元素,以及其他更改應用程式的操作、
- 不要在componentWillUpdate生命周期中重復呼叫setState方法更改狀態、
- 不要在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檔案
- main.[hash].chunk.js:這是你的應用程式代碼,App.js 等.
- 1.[hash].chunk.js:這是第三方庫的代碼,包含你在 node_modules 中匯入的模塊.
- 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
優化后的
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/373929.html
標籤:JavaScript
上一篇:超基礎的機器學習入門-原理篇



