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