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ù)測,以及列表內(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è)請求函數(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
,隨著長期項(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> ); }
或者另一種簡潔的方式使用空標(biāo)簽 <></>
代替也是一樣的效果:
const TestComponent = () => { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); }
另外還有一些實(shí)用的場景,根據(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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react組件從搭建腳手架到在npm發(fā)布的步驟實(shí)現(xiàn)
這篇文章主要介紹了react組件從搭建腳手架到在npm發(fā)布的步驟實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01react使用antd的上傳組件實(shí)現(xiàn)文件表單一起提交功能(完整代碼)
最近在做一個(gè)后臺(tái)管理項(xiàng)目,涉及到react相關(guān)知識(shí),項(xiàng)目需求需要在表單中帶附件提交,怎么實(shí)現(xiàn)這個(gè)功能呢?下面小編給大家?guī)砹藃eact使用antd的上傳組件實(shí)現(xiàn)文件表單一起提交功能,一起看看吧2021-06-06在react中對(duì)less實(shí)現(xiàn)scoped配置方式
這篇文章主要介紹了在react中對(duì)less實(shí)現(xiàn)scoped配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11react如何使用mobx6動(dòng)態(tài)加載數(shù)據(jù)
MobX是一個(gè)強(qiáng)大而簡單的狀態(tài)管理工具,它可以幫助我們更好地組織和管理React應(yīng)用程序中的數(shù)據(jù)流,本文給大家介紹react如何使用mobx6動(dòng)態(tài)加載數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧2024-02-02在react-antd中彈出層form內(nèi)容傳遞給父組件的操作
這篇文章主要介紹了在react-antd中彈出層form內(nèi)容傳遞給父組件的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10如何使用 React Router v6 在 React 中
面包屑在網(wǎng)頁開發(fā)中的角色不可忽視,它們?yōu)橛脩籼峁┝艘环N跟蹤其在網(wǎng)頁中當(dāng)前位置的方法,并有助于網(wǎng)頁導(dǎo)航,本文介紹了如何使用react-router v6和bootstrap在react中實(shí)現(xiàn)面包屑,感興趣的朋友一起看看吧2024-09-09深入React?18源碼useMemo?useCallback?memo用法及區(qū)別分析
這篇文章主要為大家介紹了React?18源碼深入分析useMemo?useCallback?memo用法及區(qū)別,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04React18之update流程從零實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了React18之update流程從零實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01