換了新公司,工做中使用的技術棧也從Vue
換到了React
,做爲一個React
新人,常常的總結和思考才能更快更好的瞭解這個框架。這裏分享一下我這兩個月來使用React
總結的一些性能優化的方法。css
由於目前公司的項目是全面擁抱hooks
的,因此只會涉及function
組件寫法,不包含class
組件寫法的相關內容。注意:本文只涉及到一些業務開發層面的代碼優化,不少通用的優化思想,好比虛擬列表,圖片懶加載,節流防抖,webpack優化等等內容都不會涉及到。html
要來優化代碼,首先咱們來簡單瞭解一下React
的更新機制。看下圖react
咱們重點來看第一步到第二步這個過程,當一個組件的props
或state
改變的時候,當前組件會從新render
。當前組件下面的全部子、孫、曾孫。。。組件不論是否用到了父組件的props
,全都會從新render
。這是一個跟Vue
更新原理很大的區別,Vue
中全部組件的渲染都是由各自的data
控制,父組件跟子組件的渲染都是獨立的。webpack
本文關於React
的性能優化,主要是三塊內容,web
這個跟React
的diff
算法有關,是一個很簡單,能夠做爲必須遵照規範的一個優化。算法
在全部的須要遍歷的列表當中,都加上一個key
值,這個值不能是那種遍歷時候的序號,必須是一個固定值。好比該條數據id
。緩存
這個key
能夠幫助diff算法更好的複用dom
元素,而不是銷燬再從新生成。性能優化
由於React
的diff
算法跟Vue
同樣是對於虛擬dom
從父到子,一層層同級的比較。因此減小節點的嵌套,能夠有效的減小diff
算法的計算量。markdown
<div className="root">
<div>
<h1>個人名字:{name}</h1>
</div>
<div>
<p>個人簡介: {content}</p>
</div>
</div>
// 徹底能夠精簡爲
<div className="root">
<h1>個人名字:{name}</h1>
<p>個人簡介: {content}</p>
</div>
複製代碼
不須要把全部狀態都放在組件的state
中,只有那些須要響應式的數據才應該存入state
。框架
在React
中處理樣式有三種
對於css Module
和css in js
來講,其實都有優缺點,用哪一個其實都沒問題。雖然不少人說css Module
性能要比css in js
好,可是那點性能真的不值一提。
這邊要說的是內聯css
,若是你沒有那種必須經過控制style
來修改組件內容或者樣式的需求的話,千萬不要寫。
這塊在後面render
的優化中會細講。
來看一個例子
import React from 'react';
export default function App() {
const [num, setNum] = useState(0);
const [factorializeNum, setFactorializeNum] = useState(5);
// 階乘函數
const factorialize = (): Number => {
console.log('觸發了');
let result = 1;
for (let i = 1; i <= factorializeNum; i++) {
result *= i;
}
return result;
};
return (
<> {num} <button onClick={() => setNum(num + factorialize())}>修改num</button> <button onClick={() => setFactorializeNum(factorializeNum + 1)}>修改階乘參數</button> </>
);
}
複製代碼
在這個組件裏,每次點擊修改num
這個按鈕,都會打印一次觸發了
,階乘函數會從新計算一遍。可是其實參數是沒有變化的,返回的結果也是沒有變化的。
咱們可使用useMemo
來緩存計算結果,避免重複計算。
import React, { useMemo } from 'react';
export default function App() {
const [num, setNum] = useState(0);
const [factorializeNum, setFactorializeNum] = useState(5);
// 當factorializeNum值不變的時候,這個函數不會再重複觸發了
const factorialize = useMemo((): Number => {
console.log('觸發了');
let result = 1;
for (let i = 1; i <= factorializeNum; i++) {
result *= i;
}
return result;
}, [factorializeNum]);
return (
<> {num} <button onClick={() => setNum(num + factorialize())}>修改num</button> <button onClick={() => setFactorializeNum(factorializeNum + 1)}>修改階乘參數</button> </>
);
}
複製代碼
咱們寫一些組件的時候常常會碰到這種需求,根據參數的不一樣,渲染不一樣的組件。例
const App = () => {
const [type, setType] = useState(1);
if (type === 1) {
return (
<> <Component1>component1</Component1> <Component2>component2</Component2> <Component3>component3</Component3> </>
);
}
return (
<Component2>component2</Component2>
<Component3>component3</Component3>
);
};
複製代碼
上面的代碼乍一看其實沒啥問題,根據類型的不一樣,返回不一樣的組件。可是對於diff
算法來講,它會對同級的新舊節點進行比較,當類型變化的時候,Component1
沒有生成了,對於diff
算法來講,他會拿舊的第一項Component1
跟新的第一項Component2
比較,由於沒有key
,並且這是組件, diff
算法會深刻到組件的子元素中再去同級比較。假設這三個組件都是不同的,diff
算法就會把舊節點的三個組件所有銷燬,再從新生成兩個新組件。
可是按性能來講,其實只須要銷燬第一個組件,複用剩下的那兩個就能夠。
加key
固然能夠,可是咱們可使用更簡單的方式。
<>
{type === 1 && <Component1>component1</Component1>}
<Component2>component2</Component2>
<Component3>component3</Component3>
</>
複製代碼
當類型不符合的時候,·component1
的位置會放置一個null
,diff
算法會拿這個null
跟舊的component1
進行比較,剩下的兩個組件順序不變,diff
算法會進行復用。並且這種方式,代碼也更加精簡。
最典型場景是tab
頁面切換,當tab
切換到相應的頁面上時,再去加載相應頁面的組件js。
這些的組件資源不會包含在主包裏,在後續在用戶須要的時候,再去加載相關的組件js資源。能夠提升頁面的加載速度,減小無效資源的加載。
主要用到兩個方法React.Suspense
和React.lazy
import React from 'react';
export default (props) => {
return (
<> <Drawer> <Tabs defaultActiveKey="1"> <TabPane> <React.Suspense fallback={<Loading />}> {React.lazy(() => import('./Component1'))} </React.Suspense> </TabPane> <TabPane> <React.Suspense fallback={<Loading />}> {React.lazy(() => import('./Component2'))} </React.Suspense> </TabPane> </Tabs> </Drawer> </>
);
};
複製代碼
使用上面的方法以後,webpack
會把這個import
的組件單獨打包成一個js
。在tab
切換到相應的頁面時,加載這個js
,渲染出相應的組件。
咱們先來看個例子
import React from 'react';
const Child = () => {
console.log('觸發Child組件渲染');
return (
<h1>這是child組件的渲染內容!</h1>
)
};
export default () => {
const [num, setNum] = useState(0);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> </>
);
}
複製代碼
當咱們每次點擊num加1
這個按鈕的時候,咱們都會在控制檯發現打印了一次觸發Child組件渲染
。說明Child
這個組件在咱們父組件的state
變化以後,每次都會從新render
。
咱們可使用React.memo
來避免子組件的重複render
。
import React from 'react';
const Child = React.memo(() => {
console.log('觸發Child組件渲染');
return (
<h1>這是child組件的渲染內容!</h1>
)
});
export default () => {
const [num, setNum] = useState(0);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> </>
);
}
複製代碼
React.memo
會判斷子組件的props
是否有改變,若是沒有,將不會重複render
。這時候咱們點擊num加1
按鈕,Child
將不會重複渲染。
咱們再來看一個例子
import React from 'react';
const Child = React.memo((props) => {
const { style } = props;
console.log('觸發Child組件渲染');
return (
<h1 style={style}>這是child組件的渲染內容!</h1>
)
});
export default () => {
const [num, setNum] = useState(0);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={{color: 'green'}}/> </>
);
}
複製代碼
這個相比較上一個例子,就是給Child
組件多傳入了一個style
參數。傳入的參數是一個靜態的對象,你以爲如今子組件會重複渲染嗎?
一開始我以爲不會,實際測試下來,發現子組件又開始了重複渲染。
state
改變,父組件從新render
的時候,像這種{color: 'green'}
會從新生成,這個對象的內存地址會變成一個新的。而React.memo
只會對props
進行淺層的比較,由於傳入對象的內存地址修改了,因此React.memo
就覺得傳入的props
有新的修改,就從新渲染了子組件。
咱們能夠有兩種方式來修改。
// 若是傳入的參數是徹底獨立的,沒有任何的耦合
// 能夠將該參數,提取到渲染函數以外
const childStyle = { color: 'green' };
export default () => {
const [num, setNum] = useState(0);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={childStyle}/> </>
);
}
// 若是傳入的參數須要使用渲染函數裏的參數或者方法
// 可使用useMemo
export default () => {
const [num, setNum] = useState(0);
const [style, setStyle] = useState('green');
// 若是不須要參數
const childStyle = useMemo(() => ({ color: 'green' }), []);
// 若是須要使用state或者方法
const childStyle = useMemo(() => ({ color: style }), [style]);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child style={childStyle}/> </>
);
}
複製代碼
函數致使子組件從新渲染的原理跟上面的內聯對象同樣,也是由於父組件的從新渲染,致使函數方法的內存地址發生變化,因此React.memo
會認爲props
有變化,致使子組件重複渲染。
咱們可使用React.useCallback
來緩存函數方法,避免子組件的重複渲染。
export default () => {
const [num, setNum] = useState(0);
const oneFnc = useCallback(() => {
console.log('這是傳入child的方法');
}, []);
return (
<> {num} <button onClick={() => setNum(num + 1)}>num加1</button> <Child onFnc={oneFnc} /> </>
);
}
複製代碼
同理,要避免在子組件的傳入參數上直接寫匿名函數。
// 不要直接寫匿名函數
<Child onFnc={() => console.log('這是傳入child的方法')} />
複製代碼
對於咱們經常使用的Context
,咱們不但可使用React.Memo
來避免子組件的重複渲染,咱們還能夠經過children
的方式。
import React, { useContext, useState } from 'react';
const DemoContext = React.createContext();
const Child = () => {
console.log('觸發Child組件渲染');
return (
<h1 style={style}>這是child組件的渲染內容!</h1>
)
};
export default () => {
const [num, setNum] = useState(0);
return (
<DemoContext.Provider value={num}> <button onClick={() => setNum(num + 1)}>num加1</button> <Child /> {...一些其餘須要使用num參數的組件} </DemoContext.Provider>
);
}
複製代碼
在這裏可使用children
方法來避免Child
的重複渲染。
import React, { useContext, useState } from 'react';
const DemoContext = React.createContext();
const Child = () => {
console.log('觸發Child組件渲染');
return (
<h1 style={style}>這是child組件的渲染內容!</h1>
)
};
function DemoComponent(props) {
const { children } = props;
const [num, setNum] = useState(0);
return (
<DemoContext.Provider value={num}> <button onClick={() => setNum(num + 1)}>num加1</button> {children} </DemoContext.Provider>
);
}
export default () => {
return (
<DemoComponent> <Child /> {...一些其餘須要使用num參數的組件} </DemoComponent>
);
}
複製代碼
這時候,修改state
,只是對於DemoComponent
這個組件內部進行render
,對於外部傳入的Child
組件,將不會重複渲染。
上面這些都是我平時開發當中真實碰到過的問題,相信也是全部React
開發者都會碰到的問題,涉及到的技術不深,但願給一些新入坑React
的同窗有所幫助。
謝謝你們的閱讀,若是以爲對你有所幫助,請幫忙點個贊支持一下!