React性能優(yōu)化的實(shí)現(xiàn)方法詳解
前言
想要寫出高質(zhì)量的代碼,僅僅靠框架底層幫我們的優(yōu)化還遠(yuǎn)遠(yuǎn)不夠,在編寫的過程中,需要我們自己去使用提高的 api,或者根據(jù)它底層的原理去做一些優(yōu)化,以及規(guī)范。
相比于 Vue ,React 不會(huì)再框架源碼層面幫助我們直接解決一下基本的性能優(yōu)化相關(guān),而是提供一下 API (Hooks)讓我們自己去優(yōu)化我們的應(yīng)用,也是它自身更靈活的一種原因之一。
下面總結(jié)了一些從編寫 React 代碼層面上能做的優(yōu)化點(diǎn)。
遍歷視圖key使用
key 的作用能夠幫助我們識(shí)別哪些元素改變了,比如添加和刪除。在 React 更新時(shí),會(huì)觸發(fā) React Diff 算法,diff 過程中過借助 key 值來判斷元素是新創(chuàng)建還是需要移動(dòng)的元素。React 會(huì)保存這個(gè)輔助狀態(tài)。從而減少不必要的元素渲染。
key 的值最好是當(dāng)前列表中擁有獨(dú)一無二的字符串。開發(fā)中通常用 id 等這些作為元素的 key 值。
當(dāng)前的列表不會(huì)發(fā)生操作,萬不得已 可以使用 index 作為 key 值。
key 應(yīng)該具有穩(wěn)定,可預(yù)測(cè),以及列表內(nèi)唯一的特質(zhì)。不穩(wěn)定的 key 比如 Math.random() 生成的會(huì) 導(dǎo)致很多組件實(shí)例和 DOM 節(jié)點(diǎn)被不必要的重新創(chuàng)建,這可能導(dǎo)致性能下降和子組件狀態(tài)丟失等等。
React.memo緩存組件
react 是單向數(shù)據(jù)流,父組件狀態(tài)的更新也會(huì)讓子組件一起重新渲染更新,即使子組件的狀態(tài)沒有發(fā)生變化,不會(huì)像 Vue 一樣能夠具體監(jiān)聽到某一個(gè)組件狀態(tài)的變化然后更新當(dāng)前的這個(gè)組件。
因此可以用 React.memo 緩存組件,這樣只有傳入當(dāng)前組件狀態(tài)值變化時(shí)才會(huì)重新渲染,值相同那么就會(huì)緩存組件。
// 子組件
const Child = React.memo(() => {
console.log("child");
return (
<div>
Child
</div>
);
});
// 父組件
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>Count++ </button>
<Child />
</div>
);
}
上面代碼 <Child /> 組件添加上 memo 每次點(diǎn)擊 count ++ 那么就會(huì)不會(huì)重新渲染了。
React.useCallback讓函數(shù)保持相同的引用
像上面的例子,如果父組件想拿到子組件的狀態(tài)值,通常會(huì)使用 callback 的方式傳遞出去給父組件。
interface ChildProps {
onChangeNum: (value: number) => void;
}
const Child: React.FC<ChildProps> = React.memo(({ onChangeNum }) => {
console.log("child");
const [num, setNum] = useState(0);
useEffect(() => {
onChangeNum(num);
}, [num]);
return (
<div>
<button
onClick={() => {
setNum((prevState) => {
return prevState + 1;
});
}}
>
Child
</button>
</div>
);
});
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h3>{count}</h3>
<button onClick={() => setCount(count + 1)}>Count++ </button>
<Child
onChangeNum={(num) => {
console.log(num, "childNum");
}}
/>
</div>
);
}
組件每次更新 num 值,父組件通過 onChangeNum 回掉函數(shù)方式接受。
注意剛才說的 memo 能夠在組件傳入值不變的情況下緩存組件避免重新渲染,但是,這里又失效了。這是為什么呢?
原因就是父組件更新了,每次都會(huì)創(chuàng)建一個(gè)新的 onChangeNum ,相當(dāng)于屬于不同的引用了,在每次 props 傳遞的回掉函數(shù)都不相同,所以 memo 失去了作用。
那么該怎么解決?那就是使用 useCallback hook 幫助我們保持相同的引用。
<Child
onChangeNum={useCallback((num) => {
console.log(num, "childNum");
}, [])}
/>
開發(fā)中使用了 memo 緩存了組件,還需要注意是否有匿名函數(shù)傳遞給子組件。
并不一定只在這種情況下才使用 useCallback ,比如一個(gè)請(qǐng)求函數(shù)或者邏輯處理函數(shù),也可以用 useCallback 包裹,不過要注意,內(nèi)部引用了外部的狀態(tài)或者值的相關(guān)聯(lián),那么需要在第二個(gè)參數(shù)也就是依賴數(shù)組里面添加上用到的某些值。
避免使用內(nèi)聯(lián)對(duì)象
在使用內(nèi)聯(lián)對(duì)象,react 每次重新渲染時(shí)會(huì)重新創(chuàng)建此對(duì)象,在更新組件對(duì)比 props ,oldProps === newProps 只要為 false 那么就會(huì) re-render 。
如果TestComponent 組件重新渲染,那么就會(huì)新建創(chuàng)建 someProps 引用。傳遞給 RootComponent 組件每次判斷新舊 props 結(jié)果不同,導(dǎo)致也重新渲染。
const TestComponent = () => {
const someProps = { value: '1' }
return <RootComponent someProps={someProps} />;
};
更好的方式是,使用 ES6 擴(kuò)展運(yùn)算符的將這個(gè)對(duì)象展開,引用類型變?yōu)橹殿愋蛡鬟f,這樣再對(duì)比 props 就會(huì)相等了。
const TestComponent = () => {
const someProps = { value: '1' }
return <RootComponent {...someProps} />;
};
使用React.useMemo緩存計(jì)算結(jié)果或者組件
如 React 文檔所說,useMemo 的基本作用是,避免每次渲染都進(jìn)行高開銷的計(jì)算。
如果是一個(gè)功能組件里面,涉及到大型的計(jì)算,組件每次重新渲染導(dǎo)致都從新調(diào)用大型的計(jì)算函數(shù),這是非常消耗性能的,我們可以使用 useMemo 來緩存這個(gè)函數(shù)的計(jì)算結(jié)果,來減少 JavaScript 在呈現(xiàn)組件期間必須執(zhí)行的工作量,來縮短阻塞主線程的時(shí)間。
// 只有當(dāng) id 發(fā)生變化的時(shí)候才會(huì)從新計(jì)算
const TestComponent = () => {
const value = useMemo(() => {
return expensiveCalculation()
}, [id])
return <Component countValue={value} />
}
在使用 useMemo 緩存計(jì)算結(jié)果之前,還需要在適當(dāng)?shù)牡胤綉?yīng)用,useMemo 也是有成本的,它也會(huì)增加整體程序初始化的耗時(shí),除非這個(gè)計(jì)算真的很昂貴,比如階乘計(jì)算。
所以并不適合全局使用,它更適合做局部的優(yōu)化。不應(yīng)該過度 useMemo。
另外在緩存結(jié)果值的同時(shí),還可以用來緩存組件。
比如有一個(gè)全局 context ,隨著長(zhǎng)期項(xiàng)目迭代 context 里面塞了很多狀態(tài),我們知道,context 的 value 發(fā)生變化,就會(huì)導(dǎo)致組件的重新渲染,而這個(gè)組件時(shí)一個(gè)很消耗性能的大型組件,只會(huì)被其中一個(gè)變量所影響才重新渲染,這時(shí)候就可以考慮使用 useMemo 進(jìn)行緩存。
const TestComponent = () => {
const appContextValue = useContext(AppContext);
const theme = appContextValue.theme;
return useMemo(() => {
return <RootComponent className={theme} />;
}, [theme]);
};
<RootComponent /> 只有在 theme 變量發(fā)生變化的時(shí)候重新渲染。
使用React.Fragment片段
react 有規(guī)定組件中必須有一個(gè)父元素,但是在某些情況下,根標(biāo)簽不需要任何的屬性,這會(huì)導(dǎo)致整個(gè)應(yīng)用程序內(nèi)創(chuàng)建許多無用的元素,那么這個(gè)標(biāo)簽的作用并沒有太大的意義。
const TestComponent = () => {
return (
<div>
<ChildA />
<ChildB />
<ChildC />
</div>
);
}
實(shí)際上頁面上的元素越多,DOM結(jié)構(gòu)嵌套越深,加載所需的時(shí)間就越多,也會(huì)增加瀏覽器的渲染壓力。
因此 React 提供了 Fragment 組件來代替包裹外層,它不會(huì)幫我們額外的創(chuàng)建外層 div 標(biāo)簽。
const TestComponent = () => {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
或者另一種簡(jiǎn)潔的方式使用空標(biāo)簽 <></> 代替也是一樣的效果:
const TestComponent = () => {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
另外還有一些實(shí)用的場(chǎng)景,根據(jù)條件渲染元素
const TestComponent = () => {
const { isLogin, name } = useApp();
return (
<>
{isLogin ? (
<>
<h3>Welcome {name}</h3>
<p>You are logged in!</p>
</>
) : (
<h3>go login...</h3>
)}
</>
);
};
組件懶加載
應(yīng)用程序初始化加載的快慢也跟組件的數(shù)量有關(guān),因此在初始化的時(shí)候,一些我們看不見的頁面,也就是最開始用不到的組件可以選擇延遲加載組件,我們可以想到的是路由的懶加載,這樣來提升頁面的加載速度和響應(yīng)時(shí)間。
react 提供了 React.Lazy 和 React.Suspense 來幫我們實(shí)現(xiàn)組件的懶加載。
import React, { lazy, Suspense } from 'react';
const AvatarComponent = lazy(() => import('./AvatarComponent'));
const renderLoader = () => <p>Loading</p>;
const DetailsComponent = () => (
<Suspense fallback={renderLoader()}>
<AvatarComponent />
</Suspense>
)
Suspense 作用就是彌補(bǔ)在 Lazy 組件加載完成之前這段空白時(shí)間所能做的事情,尤其在組件較大,或者在較弱的設(shè)備和網(wǎng)絡(luò)中,就可以通過 fallback 屬性添加一個(gè) loading 提示用戶正在加載的狀態(tài)。異步組件加載完成之后就會(huì)顯示出來。
如果單獨(dú)使用 lazy React 會(huì)在控制臺(tái)發(fā)出錯(cuò)誤提示!
通過 CSS 加載和卸載組件
渲染是昂貴的,如果頻繁加載/卸載‘很重’的組件,這個(gè)操作可能非常消耗性能或者導(dǎo)致延遲。正常情況下,我們都會(huì)用三元運(yùn)算符在判斷加載顯示,也導(dǎo)致了一個(gè)問題,每次頻繁更新,觸發(fā)加載不同的組件,就會(huì)有一定的性能損耗。這時(shí)我們可以使用 CSS 屬性將其隱藏,讓 DOM 能夠保留在頁面當(dāng)重。
**不過這種方式并不是萬能的,可能會(huì)導(dǎo)致一些布局或者窗口發(fā)生錯(cuò)位的問題。**但我們應(yīng)該選擇在不是這種情況下使用調(diào)整CSS的方法。另外一點(diǎn),將不透明度調(diào)整為0對(duì)瀏覽器的成本消耗幾乎為0(因?yàn)樗粫?huì)導(dǎo)致重排),并且應(yīng)盡可能優(yōu)先于更該visibility 和 display。
// 避免對(duì)大型的組件頻繁對(duì)加載和卸載
const ViewExample = () => {
const [isTest, setIsTest] = useState(true)
return (
<>
{ isTest ? <ViewComponent /> : <TestComponent />}
</>
);
};
// 使用該方式提升性能和速度
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
const ViewExample = () => {
const [isTest, setIsTest] = useState(true)
return (
<>
<ViewComponent style={!isTest ? visibleStyles : hiddenStyles} />
<TestComponent style={{ isTest ? visibleStyles : hiddenStyles }} />
</>
);
};
變與不變的地方做分離
通常使用 useMemo、useCallback 進(jìn)行優(yōu)化,這里說說不借助這些Hooks進(jìn)行優(yōu)化,
變與不變做分離的概念來源,其實(shí)就是因?yàn)樽陨淼膔eact 的機(jī)制,父組件的狀態(tài)更新了,所有的子組件得跟著一起渲染,意思是將有狀態(tài)的組件和無狀態(tài)的組件分離開。
function ExpensiveCpn() {
console.log("ExpensiveCpn");
let now = performance.now();
while (performance.now() - now < 100) {}
return <p>耗時(shí)的組件</p>;
}
export default function App() {
const [num, updateNum] = useState("");
return (
<>
<input
type="text"
onChange={(e) => updateNum(e.target.value)}
value={num}
/>
<ExpensiveCpn />
</>
);
}
上面輸入框輸入都會(huì)刷新組件<ExpensiveCpn/>,我們可以不使用 useMemo 等API就能控制渲染其實(shí)就是將變得和不變的分離開????:
function ExpensiveCpn() {
console.log("ExpensiveCpn");
let now = performance.now();
while (performance.now() - now < 100) {}
return <p>耗時(shí)的組件</p>;
}
function Input() {
const [num, updateNum] = useState("");
return (
<input
type="text"
onChange={(e) => updateNum(e.target.value)}
value={num}
/>
);
}
export default function App() {
return (
<>
<Input />
<ExpensiveCpn />
</>
);
}
這樣渲染的組件只會(huì)是 <Input/>組件內(nèi)部,不會(huì)影響到外部。
總結(jié)
上面一些方式,可以從幾個(gè)方面理解:
- 減少重新render的次數(shù):memo、useMemo、useCallback 使用、避免使用內(nèi)聯(lián)對(duì)象、變與不變的分離。
- 減少渲染的節(jié)點(diǎn):React.Fragment 片段、組件懶加載。
- 降低渲染計(jì)算量:遍歷試圖使用 key。
到此這篇關(guān)于React性能優(yōu)化的實(shí)現(xiàn)方法詳解的文章就介紹到這了,更多相關(guān)React性能優(yōu)化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react組件的創(chuàng)建與更新實(shí)現(xiàn)流程詳解
React組件分為函數(shù)組件與class組件;函數(shù)組件是無狀態(tài)組件,class稱為類組件;函數(shù)組件只有props,沒有自己的私有數(shù)據(jù)和生命周期函數(shù);class組件有自己私有數(shù)據(jù)(this.state)和生命周期函數(shù)2022-10-10
詳解超簡(jiǎn)單的react服務(wù)器渲染(ssr)入坑指南
這篇文章主要介紹了詳解超簡(jiǎn)單的react服務(wù)器渲染(ssr)入坑指南,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-02-02
React中Refs的使用場(chǎng)景及核心要點(diǎn)詳解
在使用?React?進(jìn)行開發(fā)過程中,或多或少使用過?Refs?進(jìn)行?DOM?操作,這篇文章主要介紹了?Refs?功能和使用場(chǎng)景以及注意事項(xiàng),希望對(duì)大家有所幫助2023-07-07
React項(xiàng)目中應(yīng)用TypeScript的實(shí)現(xiàn)
TypeScript通常都會(huì)依賴于框架,例如和vue、react 這些框架結(jié)合,本文就主要介紹了React項(xiàng)目中應(yīng)用TypeScript的實(shí)現(xiàn),分享給大家,具體如下:2021-09-09
詳解React中多種組件通信方式的實(shí)現(xiàn)
在React中,組件之間的通信是一個(gè)非常重要的話題,React提供了幾種方式來實(shí)現(xiàn)跨組件通信,下面小編將詳細(xì)講講其中幾種通信方式,并提供實(shí)際的代碼示例,需要的可以參考下2023-11-11
用react實(shí)現(xiàn)一個(gè)簡(jiǎn)單的scrollView組件
這篇文章主要給大家介紹一下如何用 react 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 scrollView組件,文中有詳細(xì)的代碼示例,具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07
react組件封裝input框的防抖處理的項(xiàng)目實(shí)現(xiàn)
本文主要介紹了react組件封裝input框的防抖處理的項(xiàng)目實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
TypeScript在React中的應(yīng)用技術(shù)實(shí)例解析
這篇文章主要為大家介紹了TypeScript在React中的應(yīng)用技術(shù)實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

