一萌狂、瀏覽器渲染過(guò)程
- 瀏覽器重繪(Repaint)和回流(Reflow)
重繪(Repaint):當(dāng)頁(yè)面中元素樣式的改變并不影響它在文檔流中的位置時(shí)(例如:color谷丸、background-color、visibility 等)来惧,瀏覽器會(huì)將新樣式賦予給元素并重新繪制它敢订,這個(gè)過(guò)程稱為重繪(Repaint)。
回流(Reflow):當(dāng) Render Tree 中部分或全部元素的尺寸描滔、結(jié)構(gòu)、或某些屬性發(fā)生改變時(shí)踪古,瀏覽器重新渲染部分或全部文檔的過(guò)程稱為回流含长。
回流必將引起重繪,重繪不一定會(huì)引起回流伏穆。
會(huì)導(dǎo)致回流的操作:
- 頁(yè)面首次渲染
- 瀏覽器窗口大小發(fā)生改變
- 元素尺寸或位置發(fā)生改變?cè)貎?nèi)容變化(文字?jǐn)?shù)量或圖片大小等等)
- 元素字體大小變化
- 添加或者刪除可見(jiàn)的 DOM 元素
- 激活 CSS 偽類(例如:hover)
- 查詢某些屬性或調(diào)用某些方法
- 一些常用且會(huì)導(dǎo)致回流的屬性和方法
clientWidth拘泞、clientHeight、clientTop枕扫、clientLeftoffsetWidth陪腌、offsetHeight、offsetTop、offsetLeftscrollWidth偷厦、scrollHeight商叹、scrollTop、scrollLeftscrollIntoView()只泼、scrollIntoViewIfNeeded()、getComputedStyle()卵洗、
getBoundingClientRect()请唱、scrollTo()
性能影響
回流比重繪的代價(jià)要更高。
有時(shí)即使僅僅回流一個(gè)單一的元素过蹂,它的父元素以及任何跟隨它的元素也會(huì)產(chǎn)生回流∈螅現(xiàn)代瀏覽器會(huì)對(duì)頻繁的回流或重繪操作進(jìn)行優(yōu)化:瀏覽器會(huì)維護(hù)一個(gè)隊(duì)列,把所有引起回流和重繪的操作放入隊(duì)列中酷勺,如果隊(duì)列中的任務(wù)數(shù)量或者時(shí)間間隔達(dá)到一個(gè)閾值的本橙,瀏覽器就會(huì)將隊(duì)列清空,進(jìn)行一次批處理脆诉,這樣可以把多次回流和重繪變成一次甚亭。
使用 transform 和 opacity 屬性更改來(lái)實(shí)現(xiàn)動(dòng)畫
在 CSS 中,transforms 和 opacity 這兩個(gè)屬性更改不會(huì)觸發(fā)重排與重繪击胜,它們是可以由合成器(composite)單獨(dú)處理的屬性亏狰。
二、網(wǎng)頁(yè)交互
- 防抖(debounce)/節(jié)流(throttle)
防抖是控制次數(shù)偶摔,節(jié)流時(shí)控制頻率暇唾,如果事件一直觸發(fā),則防抖下函數(shù)在中間不會(huì)執(zhí)行(最后執(zhí)行一次)辰斋,節(jié)流下函數(shù)按照一定的頻率執(zhí)行策州。
debounce:在第一次觸發(fā)事件時(shí),不立即執(zhí)行函數(shù)宫仗,而是給出一個(gè)期限值比如200ms够挂,然后:
如果在200ms內(nèi)沒(méi)有再次觸發(fā)事件,那么就執(zhí)行函數(shù)
如果在200ms內(nèi)再次觸發(fā)事件锰什,那么當(dāng)前的計(jì)時(shí)取消下硕,重新開(kāi)始計(jì)時(shí)
效果:如果短時(shí)間內(nèi)大量觸發(fā)同一事件,只會(huì)執(zhí)行一次函數(shù)汁胆。
實(shí)現(xiàn):
function debounce(fn,delay){
let timer = null;
return function () {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(fn,delay);
}
}
定義:對(duì)于短時(shí)間內(nèi)連續(xù)觸發(fā)的事件(如滾動(dòng)事件)梭姓,在 n 秒內(nèi)函數(shù)只能執(zhí)行一次,如果在 n 秒內(nèi)又觸發(fā)了事件嫩码,則會(huì)重新計(jì)算函數(shù)執(zhí)行時(shí)間
節(jié)流:讓函數(shù)執(zhí)行一次后誉尖,在某個(gè)時(shí)間段內(nèi)暫時(shí)失效,過(guò)了這段時(shí)間后再重新激活
效果:如果短時(shí)間內(nèi)大量觸發(fā)同一事件铸题,那么在函數(shù)執(zhí)行一次之后铡恕,該函數(shù)在指定的時(shí)間期限內(nèi)不再工作琢感,直至過(guò)了這段時(shí)間才重新生效。
實(shí)現(xiàn):
funtion throttler(fn, delay) {
let valid = true;
return function () {
if(!valid) {
return false;
}
valid = false;
setTimeout( () => {
fn();
valid = true;
},delay)
}
}
/* 請(qǐng)注意探熔,節(jié)流函數(shù)并不止上面這種實(shí)現(xiàn)方案,
例如可以完全不借助setTimeout驹针,可以把狀態(tài)位換成時(shí)間戳,然后利用時(shí)間戳差值是否大于指定間隔時(shí)間來(lái)做判定诀艰。
也可以直接將setTimeout的返回的標(biāo)記當(dāng)做判斷條件-判斷當(dāng)前定時(shí)器是否存在柬甥,如果存在表示還在冷卻,并且在執(zhí)行fn之后消除定時(shí)器表示激活其垄,原理都一樣
*/
定義:對(duì)于短時(shí)間內(nèi)連續(xù)觸發(fā)的事件苛蒲,函數(shù)執(zhí)行一次后會(huì)暫時(shí)失效,過(guò)了n秒后才能再次生效绿满。
三臂外、React性能優(yōu)化
三(一)React渲染優(yōu)化
props的改變(被動(dòng)渲染),和state改變(主動(dòng)渲染)會(huì)導(dǎo)致react進(jìn)行重新渲染(進(jìn)行dom diff)
1. 綁定事件盡量不要使用箭頭函數(shù)喇颁,即不要在jsx內(nèi)寫內(nèi)聯(lián)函數(shù)
<Child handerClick={() => { this.handleClick() }}/>
原因有:每一次渲染更新這段jsx都會(huì)產(chǎn)生新的函數(shù)對(duì)象(即使直接綁定的DOM元素)漏健;
Child 會(huì)因?yàn)檫@個(gè)新生成的箭頭函數(shù)而進(jìn)行更新,每一次的props都是新的无牵,在未給Child任何特殊更新限定條件的時(shí)候,從而產(chǎn)生Child 組件的不必要渲染
解決問(wèn)題:類組件(有狀態(tài)組件)綁定事件直接指向類中的函數(shù)漾肮;函數(shù)組件(無(wú)狀態(tài)組件)函數(shù)定義使用useMemo/useCallback
有狀態(tài)組件
class index extends React.Component{
handerClick=()=>{
console.log(666)
}
handerClick1=()=>{
console.log(777)
}
render(){
return <div>
<ChildComponent handerClick={ this.handerClick } />
<div onClick={ this.handerClick1 } >hello,world</div>
</div>
}
}
無(wú)狀態(tài)組件
function index(){
const handerClick1 = useMemo(()=>()=>{
console.log(777)
},[]) /* [] 存在當(dāng)前 handerClick1 的依賴項(xiàng)*/
const handerClick = useCallback(()=>{ console.log(666) },[]) /* [] 存在當(dāng)前 handerClick 的依賴項(xiàng)*/
return <div>
<ChildComponent handerClick={ handerClick } />
<div onClick={ handerClick1 } >hello,world</div>
</div>
}
2. 不要使用inline定義的方法或Object為props傳值 (傳遞給子組件的固定對(duì)象提前定義好一個(gè)變量)
// 每一次渲染都會(huì)產(chǎn)生一個(gè)對(duì)象給style
// 都會(huì)被認(rèn)為是一個(gè)style這個(gè)prop發(fā)生了變化,因?yàn)槭且粋€(gè)新的對(duì)象
<Foo style={{ color:"red" }}
// 對(duì)象提前定義
const fooStyle = { color: "red" }; // 可以放在構(gòu)造函數(shù)中或者函數(shù)組件外面
<Foo style={fooStyle} />
3. 循環(huán)/列表正確使用key
key目的就是在一次循環(huán)中茎毁,找到與新節(jié)點(diǎn)對(duì)應(yīng)的老節(jié)點(diǎn)克懊,復(fù)用節(jié)點(diǎn),節(jié)省開(kāi)銷
增加key后七蜘,React就不是diff谭溉,而是直接使用insertBefore操作移動(dòng)組件位置,而這個(gè)操作是移動(dòng)DOM節(jié)點(diǎn)最高效的辦法
在選取Key值時(shí)盡量不要用索引號(hào)橡卤,因?yàn)槿绻?dāng)數(shù)據(jù)的添加方式不是順序添加扮念,而是以其他方式(逆序,隨機(jī)等)碧库,會(huì)導(dǎo)致每一次添加數(shù)據(jù)削锰,每一個(gè)數(shù)據(jù)值的索引號(hào)都不一樣廷支,這就導(dǎo)致了Key的變化拣凹,而當(dāng)Key變化時(shí)础浮,React就會(huì)認(rèn)為這與之前的數(shù)據(jù)值不相同,會(huì)多次執(zhí)行渲染沽瞭,會(huì)造成大量的性能浪費(fèi)
4. 組件盡可能的細(xì)分迁匠,比如一個(gè)input+list組件,可以將list分成一個(gè)PureComponent,只在list數(shù)據(jù)變化時(shí)更新城丧。否則在input值變化頁(yè)面重新渲染的時(shí)候延曙,list也需要進(jìn)行不必要的DOM diff
當(dāng)一個(gè)組件的props或者state改變時(shí),React通過(guò)比較新返回的元素和之前渲染的元素來(lái)決定是否有必要更新實(shí)際的DOM亡哄。當(dāng)他們不相等時(shí)枝缔,React會(huì)更新DOM。
在一些情況下蚊惯,你的組件可以通過(guò)重寫這個(gè)生命周期函數(shù)shouldComponentUpdate來(lái)提升速度魂仍, 它是在重新渲染過(guò)程開(kāi)始前觸發(fā)的。 這個(gè)函數(shù)默認(rèn)返回true拣挪,可使React執(zhí)行更新。
5. 減少不必要的props引起的重繪
在 class Component 中可以使用shouldComponentUpdate或者PureComponent減少重復(fù)渲染俱诸。
Hooks組件則可以使用React.memo
普通的 React.memo和PureComponent很像,對(duì)props做一層淺比較菠劝,如果沒(méi)發(fā)生變更則不執(zhí)行重繪
const Child = ()=><span>test</span>
export default React.memo(Child);
React.memo支持第二個(gè)參數(shù)compare, 返回 true 時(shí),不會(huì)觸發(fā)更新睁搭,來(lái)手動(dòng)判斷是否需要渲染赶诊。例如對(duì)于一個(gè)普通受控組件,當(dāng)defaultValue發(fā)生變更時(shí)無(wú)需重繪組件园骆,所以可以用下面的代碼實(shí)現(xiàn)舔痪。
function MyComponent({ defaultValue, value, onChange }) {
return null;
}
function compare(prevProps, props) {
return prevProps.value === props.value &&
prevProps.onChange === props.onChange;
}
export default React.memo(MyComponent, compare);
6. 減少不必要state引起的重繪
useState的一條最佳實(shí)踐是將state盡可能的顆粒化锌唾,但是在異步回調(diào)中同時(shí)更新多個(gè)狀態(tài)時(shí)會(huì)觸發(fā)多次渲染(在 react 的 event handler 內(nèi)部同步的多次 setState 會(huì)被 batch 為一次更新锄码,但是在一個(gè)異步的事件循環(huán)里面多次 setState,react 不會(huì) batch晌涕。可以使用 ReactDOM.unstable_batchedUpdates 來(lái)強(qiáng)制 batch)
import { unstable_batchUpdateds } from 'react-dom'滋捶;
function Home() {
const [userInfo, setUserInfo] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
asyncFetchUser().then(() => {
// ------------------
unstable_batchUpdateds(()=>{
setUserInfo({});
setLoading(false);
})
// -----------------
});
}, []);
console.log('render');
return null;
}
7. 善用useMemo/useCallback/React.memo
對(duì)于無(wú)狀態(tài)組件,數(shù)據(jù)更新就等于函數(shù)上下文的重復(fù)執(zhí)行余黎。那么函數(shù)里面的變量重窟,方法就會(huì)重新聲明,可以用useMemo/useCallback將常量或者函數(shù)緩存下來(lái)避免重復(fù)執(zhí)行
8. 合理使用狀態(tài)管理(dva/redux-sage/...)
狀態(tài)管理的主要作用:一 就是解決跨層級(jí)組件通信問(wèn)題 。二 就是對(duì)一些全局公共狀態(tài)的緩存惧财。
自身組件單獨(dú)的數(shù)據(jù)可直接再組件內(nèi)部請(qǐng)求數(shù)據(jù)巡扇,避免數(shù)據(jù)走了一遍狀態(tài)管理,最終還是回到了組件本身的情況
三(二)React獲取數(shù)據(jù)優(yōu)化
使用reselect庫(kù):
在使用Redux進(jìn)行數(shù)據(jù)的傳遞時(shí),特別是經(jīng)常有重復(fù)性的數(shù)據(jù)傳遞操作時(shí)垮衷,可以使用reselect庫(kù)在內(nèi)部對(duì)數(shù)據(jù)進(jìn)行緩存處理厅翔,在重復(fù)調(diào)用時(shí)便可使用緩存快速加載,加強(qiáng)性能
React hooks 對(duì)比class優(yōu)點(diǎn)
- Hooks組件復(fù)用邏輯相比高階組件復(fù)用邏輯更易維護(hù)
比如componentDidMount/shouldComponentUpdate中帘靡,可能就會(huì)有大量邏輯代碼知给,包括網(wǎng)絡(luò)請(qǐng)求,一些事件的監(jiān)聽(tīng),hooks的好處: 面向生命周期編程變成了面向業(yè)務(wù)邏輯編程涩赢,便于維護(hù) - hook可以將state細(xì)微化戈次,便于組件的拆分, class時(shí)期筒扒,setState后需要對(duì)比整個(gè)虛擬dom的狀態(tài)怯邪,對(duì)一個(gè)復(fù)雜頁(yè)面,幾十個(gè)狀態(tài)需要對(duì)比耗費(fèi)性能花墩。而hook階段只需要對(duì)比一個(gè)值即可悬秉,性能更佳。
用hook代替高階組件
- useSelector/useDispatch代替高階函數(shù)connect
import { useDispatch, useSelector } from 'react-redux';
//....
const dispatch = useDispatch();
useEffect (()=>{
dispatch({
type:'', // ...
})
},[])
// ....
const { progressList, loading } = useSelector(
({
dataManage,
loading,
}: {
dataManage: any;
loading: any;
}) => ({
...dataManage,
loading: loading.effects['dataManage/fetchProgressList'],
}),
);
return .....