網站首頁 編程語言 正文
前言
想要寫出高質量的代碼,僅僅靠框架底層幫我們的優化還遠遠不夠,在編寫的過程中,需要我們自己去使用提高的 api,或者根據它底層的原理去做一些優化,以及規范。
相比于 Vue ,React 不會再框架源碼層面幫助我們直接解決一下基本的性能優化相關,而是提供一下 API (Hooks)讓我們自己去優化我們的應用,也是它自身更靈活的一種原因之一。
下面總結了一些從編寫 React 代碼層面上能做的優化點。
遍歷視圖key使用
key 的作用能夠幫助我們識別哪些元素改變了,比如添加和刪除。在 React 更新時,會觸發 React Diff 算法,diff 過程中過借助 key 值來判斷元素是新創建還是需要移動的元素。React 會保存這個輔助狀態。從而減少不必要的元素渲染。
key 的值最好是當前列表中擁有獨一無二的字符串。開發中通常用 id 等這些作為元素的 key 值。
當前的列表不會發生操作,萬不得已 可以使用 index 作為 key 值。
key 應該具有穩定,可預測,以及列表內唯一的特質。不穩定的 key 比如 Math.random() 生成的會 導致很多組件實例和 DOM 節點被不必要的重新創建,這可能導致性能下降和子組件狀態丟失等等。
React.memo緩存組件
react 是單向數據流,父組件狀態的更新也會讓子組件一起重新渲染更新,即使子組件的狀態沒有發生變化,不會像 Vue 一樣能夠具體監聽到某一個組件狀態的變化然后更新當前的這個組件。
因此可以用 React.memo
緩存組件,這樣只有傳入當前組件狀態值變化時才會重新渲染,值相同那么就會緩存組件。
// 子組件 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讓函數保持相同的引用
像上面的例子,如果父組件想拿到子組件的狀態值,通常會使用 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 回掉函數方式接受。
注意剛才說的 memo
能夠在組件傳入值不變的情況下緩存組件避免重新渲染,但是,這里又失效了。這是為什么呢?
原因就是父組件更新了,每次都會創建一個新的 onChangeNum ,相當于屬于不同的引用了,在每次 props 傳遞的回掉函數都不相同,所以 memo 失去了作用。
那么該怎么解決?那就是使用 useCallback
hook 幫助我們保持相同的引用。
<Child onChangeNum={useCallback((num) => { console.log(num, "childNum"); }, [])} />
開發中使用了 memo
緩存了組件,還需要注意是否有匿名函數傳遞給子組件。
并不一定只在這種情況下才使用 useCallback ,比如一個請求函數或者邏輯處理函數,也可以用 useCallback 包裹,不過要注意,內部引用了外部的狀態或者值的相關聯,那么需要在第二個參數也就是依賴數組里面添加上用到的某些值。
避免使用內聯對象
在使用內聯對象,react 每次重新渲染時會重新創建此對象,在更新組件對比 props ,oldProps === newProps
只要為 false
那么就會 re-render
。
如果TestComponent
組件重新渲染,那么就會新建創建 someProps 引用。傳遞給 RootComponent
組件每次判斷新舊 props 結果不同,導致也重新渲染。
const TestComponent = () => { const someProps = { value: '1' } return <RootComponent someProps={someProps} />; };
更好的方式是,使用 ES6 擴展運算符的將這個對象展開,引用類型變為值類型傳遞,這樣再對比 props 就會相等了。
const TestComponent = () => { const someProps = { value: '1' } return <RootComponent {...someProps} />; };
使用React.useMemo緩存計算結果或者組件
如 React 文檔所說,useMemo
的基本作用是,避免每次渲染都進行高開銷的計算。
如果是一個功能組件里面,涉及到大型的計算,組件每次重新渲染導致都從新調用大型的計算函數,這是非常消耗性能的,我們可以使用 useMemo
來緩存這個函數的計算結果,來減少 JavaScript 在呈現組件期間必須執行的工作量,來縮短阻塞主線程的時間。
// 只有當 id 發生變化的時候才會從新計算 const TestComponent = () => { const value = useMemo(() => { return expensiveCalculation() }, [id]) return <Component countValue={value} /> }
在使用 useMemo 緩存計算結果之前,還需要在適當的地方應用,useMemo 也是有成本的,它也會增加整體程序初始化的耗時,除非這個計算真的很昂貴,比如階乘計算。
所以并不適合全局使用,它更適合做局部的優化。不應該過度 useMemo。
另外在緩存結果值的同時,還可以用來緩存組件。
比如有一個全局 context
,隨著長期項目迭代 context 里面塞了很多狀態,我們知道,context 的 value 發生變化,就會導致組件的重新渲染,而這個組件時一個很消耗性能的大型組件,只會被其中一個變量所影響才重新渲染,這時候就可以考慮使用 useMemo 進行緩存。
const TestComponent = () => { const appContextValue = useContext(AppContext); const theme = appContextValue.theme; return useMemo(() => { return <RootComponent className={theme} />; }, [theme]); };
<RootComponent />
只有在 theme
變量發生變化的時候重新渲染。
使用React.Fragment片段
react 有規定組件中必須有一個父元素,但是在某些情況下,根標簽不需要任何的屬性,這會導致整個應用程序內創建許多無用的元素,那么這個標簽的作用并沒有太大的意義。
const TestComponent = () => { return ( <div> <ChildA /> <ChildB /> <ChildC /> </div> ); }
實際上頁面上的元素越多,DOM結構嵌套越深,加載所需的時間就越多,也會增加瀏覽器的渲染壓力。
因此 React 提供了 Fragment
組件來代替包裹外層,它不會幫我們額外的創建外層 div
標簽。
const TestComponent = () => { return ( <React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> ); }
或者另一種簡潔的方式使用空標簽 <></>
代替也是一樣的效果:
const TestComponent = () => { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); }
另外還有一些實用的場景,根據條件渲染元素
const TestComponent = () => { const { isLogin, name } = useApp(); return ( <> {isLogin ? ( <> <h3>Welcome {name}</h3> <p>You are logged in!</p> </> ) : ( <h3>go login...</h3> )} </> ); };
組件懶加載
應用程序初始化加載的快慢也跟組件的數量有關,因此在初始化的時候,一些我們看不見的頁面,也就是最開始用不到的組件可以選擇延遲加載組件,我們可以想到的是路由的懶加載,這樣來提升頁面的加載速度和響應時間。
react 提供了 React.Lazy
和 React.Suspense
來幫我們實現組件的懶加載。
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
組件加載完成之前這段空白時間所能做的事情,尤其在組件較大,或者在較弱的設備和網絡中,就可以通過 fallback
屬性添加一個 loading 提示用戶正在加載的狀態。異步組件加載完成之后就會顯示出來。
如果單獨使用 lazy
React 會在控制臺發出錯誤提示!
通過 CSS 加載和卸載組件
渲染是昂貴的,如果頻繁加載/卸載‘很重’的組件,這個操作可能非常消耗性能或者導致延遲。正常情況下,我們都會用三元運算符在判斷加載顯示,也導致了一個問題,每次頻繁更新,觸發加載不同的組件,就會有一定的性能損耗。這時我們可以使用 CSS 屬性將其隱藏,讓 DOM 能夠保留在頁面當重。
**不過這種方式并不是萬能的,可能會導致一些布局或者窗口發生錯位的問題。**但我們應該選擇在不是這種情況下使用調整CSS的方法。另外一點,將不透明度調整為0對瀏覽器的成本消耗幾乎為0(因為它不會導致重排),并且應盡可能優先于更該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 進行優化,這里說說不借助這些Hooks進行優化,
變與不變做分離的概念來源,其實就是因為自身的react 的機制,父組件的狀態更新了,所有的子組件得跟著一起渲染,意思是將有狀態的組件和無狀態的組件分離開。
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/>
組件內部,不會影響到外部。
總結
上面一些方式,可以從幾個方面理解:
- 減少重新render的次數:memo、useMemo、useCallback 使用、避免使用內聯對象、變與不變的分離。
- 減少渲染的節點:React.Fragment 片段、組件懶加載。
- 降低渲染計算量:遍歷試圖使用 key。
原文鏈接:https://blog.csdn.net/weixin_42164539/article/details/128779412
相關推薦
- 2022-04-01 docker 命令查看registry倉庫鏡像
- 2023-10-09 axios二次封裝 大文件分片上傳的封裝
- 2022-08-07 C#中struct與class的區別詳解_C#教程
- 2022-08-06 Python中if?__name__==‘__main__‘用法詳情_python
- 2023-01-01 C語言實現輸出各種三角形_C 語言
- 2022-07-16 SpringMVC @Controller和@RequestMapping注解
- 2023-04-01 一文初探?Goroutine?與?channel基本用法_Golang
- 2023-12-09 【設計模式-3.1】結構型——外觀模式
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支