React中組件優(yōu)化的最佳方案分享
核心:減少渲染真實DOM的頻率,以及減少VD比對的頻率
1. 組件卸載前執(zhí)行清理操作
- 注冊的全局事件
- 定時器
證明:在組件掛載之后通過useEffect中開啟定時器,銷毀此組件之后,定時器還是存在的!
基于此,需要在useEffect第一個形參函數(shù)的返回值中將定時器清除掉。
2. 通過純組件來提升性能
什么是純組件
所謂純組件,就是當輸入數(shù)據(jù)發(fā)生改變的時候,會將新數(shù)據(jù)和舊數(shù)據(jù)進行一次淺層比較,如果淺層比較結(jié)果相同,那么就不會引起重新渲染。
如何實現(xiàn)純組件
使用PureComponent類或者memo方法可以實現(xiàn)純的類或者函數(shù)組件。
驗證示例
import React from "react";
export default class App extends React.Component {
constructor() {
super();
this.state = { name: "張三" };
}
updateName() {
setInterval(() => this.setState({ name: "張三" }), 1000);
}
componentDidMount() {
this.updateName();
}
render() {
return (
<div>
<RegularComponent name={this.state.name} />
{/* 這里可能有其他組件或JSX元素 */}
</div>
);
}
}
3. 在類組件中使用shouldComponentUpdate
由于使用PureComponent只能進行淺層的比較,所以在類組件中使用shouldComponentUpdate生命周期函數(shù)能夠自定義用戶的比較行為。
此生命周期函數(shù)的返回值是一個布爾值,如果為true表示需要更新,反之則不需要進行更新;此函數(shù)接受兩個參數(shù),其一是nextProps,另外一個nextState。分別表示外部和內(nèi)部數(shù)據(jù)。
import React from "react";
export default class App extends React.Component {
constructor() {
super();
this.state = {
person: {
name: "張三",
age: 20,
job: "waiter"
}
};
}
componentDidMount() {
setTimeout(() => {
// 這里使用擴展運算符合并對象來確保我們創(chuàng)建了person對象的一個新副本
this.setState({ person: { ...this.state.person, job: "chef" } });
}, 2000);
}
shouldComponentUpdate(nextProps, nextState) {
// 只在person對象的name或age屬性發(fā)生變化時更新組件
if (this.state.person.name !== nextState.person.name || this.state.person.age !== nextState.person.age) {
return true;
}
return false;
}
render() {
return (
// 此處根據(jù)你的需求可以添加任何需要顯示的內(nèi)容
<div>
Name: {this.state.person.name},
Age: {this.state.person.age},
Job: {this.state.person.job}
</div>
);
}
}
4. 通過函數(shù)式組件React.memo提升性能
父組件的渲染會引起子組件的渲染
證明示例:
import React, { useState, useEffect } from 'react';
function App() {
const [name] = useState("張三");
const [index, setIndex] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setIndex(prev => prev + 1);
}, 1000);
// 清除interval,防止內(nèi)存泄漏
return () => clearInterval(intervalId);
}, []);
return (
<div>
{index}
<ShowName name={name} />
</div>
);
}
function ShowName({ name }) {
console.log("rendering ShowName");
return <div>{name}</div>;
}
export default App;
使用memo優(yōu)化上述代碼
const ShowName = React.memo(function ({ name }) {
console.log("rendering ShowName");
return <div>{name}</div>;
})
memo具有第二個參數(shù),第二個參數(shù)也是一個函數(shù),此函數(shù)返回一個布爾值,其作用是自定義用戶的比較行為;如果返回值是true則表示比較的兩個對象(也是此函數(shù)的兩個入?yún)ⅲ謩e為: prevProps和nextProps)是相同的,因此也就不需要重新渲染;否則則需要重新渲染。
import React, { memo, useEffect, useState } from "react";
// 比較函數(shù),用以優(yōu)化渲染
function comparePerson(prevProps, nextProps) {
if (
prevProps.person.name !== nextProps.person.name ||
prevProps.person.age !== nextProps.person.age
) {
return false; // 如果person的name或age改變了,就重新渲染
}
return true; // 如果person的name或age沒變,不重新渲染
}
// 用memo包裹的組件,將比較函數(shù)作為第二個參數(shù)傳入
const ShowPerson = memo(function ShowPerson({ person }) {
console.log("rendering ShowPerson");
return (
<div>
{person.name} {person.age}
</div>
);
}, comparePerson);
function App() {
const [person, setPerson] = useState({ name: "張三", age: 20, job: "waiter" });
useEffect(() => {
const intervalId = setInterval(() => {
// 更新設置person狀態(tài)對象中的job屬性,而不是name或age
setPerson(prevPerson => ({ ...prevPerson, job: "chef" }));
}, 1000);
// 清除interval,防止內(nèi)存泄漏
return () => clearInterval(intervalId);
}, []);
return (
<div>
<ShowPerson person={person} />
</div>
);
}
export default App;
5. 使用組件的懶加載來提升組件性能
使用懶加載的組件優(yōu)化的核心邏輯在于減少bundle文件的大小,加快組件的呈現(xiàn)速度。但是,采用懶加載的組件會被打包到不同的文件中(分包)
路由組件懶加載
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Link, Route, Switch } from "react-router-dom";
// 使用React的lazy函數(shù)動態(tài)導入組件
const Home = lazy(() => import(/* webpackChunkName: "Home" */ "./Home"));
const List = lazy(() => import(/* webpackChunkName: "List" */ "./List"));
function App() {
return (
<BrowserRouter>
<Link to="/">Home</Link>
<Link to="/list">List</Link>
<Switch>
// 使用Suspense包裹Route,并提供fallback來展示加載狀態(tài)
<Suspense fallback={<div>Loading...</div>}>
<Route path="/" component={Home} exact />
<Route path="/list" component={List} />
</Suspense>
</Switch>
</BrowserRouter>
);
}
export default App;
代碼分析:
lazy是一個React函數(shù),它允許你定義一個動態(tài)加載的組件。這里,Home和List組件都是通過lazy函數(shù)和動態(tài)import進行定義的。Webpack將這些動態(tài)導入的組件分離到不同的代碼塊(chunks),當訪問對應路由時才會加載它們。Suspense組件是React內(nèi)置的一個組件,它允許你在渲染等待內(nèi)容(如懶加載組件)時顯示一些回退內(nèi)容。在這個例子中,回退內(nèi)容是一個簡單的<div>Loading...</div>,它會在懶加載組件被加載和渲染之前顯示。BrowserRouter是react-router-dom庫中的組件,它使用HTML5歷史API(pushState,replaceState和popstate事件)來保持UI和URL的同步。Link組件提供聲明式的、可訪問的導航的方式。Route是配置路由的基本單元,它將一個路徑和一個組件映射起來,當路徑匹配時就會渲染對應的組件。Switch組件用于渲染第一個匹配當前位置的<Route>或<Redirect>。
這種方式使得在應用啟動時不會加載所有組件,而是僅在用戶導航到相應的路由時才加載對應的組件,從而優(yōu)化了性能。
根據(jù)某種條件進行組件懶加載
使用條件:組件不會隨著條件頻繁切換的場景下
import React, { lazy, Suspense } from "react";
function App() {
let LazyComponent = null;
if (true) {
LazyComponent = lazy(() => import(/* webpackChunkName: "Home" */ "./Home"));
} else {
LazyComponent = lazy(() => import(/* webpackChunkName: "List" */ "./List"));
}
return (
<Suspense fallback={<div>Loading</div>}>
<LazyComponent />
</Suspense>
);
}
export default App;
6. 使用Fragment避免額外標記
那就是:<Fragment></Fragment>
7. 避免使用內(nèi)聯(lián)函數(shù)提升函數(shù)性能
原因:render函數(shù)每次執(zhí)行渲染的時候都會重新創(chuàng)建此內(nèi)斂函數(shù)的實例,導致React在進行虛擬DOM的對比的時候,同一個位置的內(nèi)斂函數(shù)并不相等,由此導致兩個消耗:新的創(chuàng)建需要消耗;舊的回收也需要消耗。
不好的實踐:
onChange={e=>this.setState({value:e.target.value})}
好的實踐:
this.handleOnChange = e=>this.setState({value:e.target.value});
...
onChange={handleOnChange}
8. 正確的更正this的指向問題
修正this指向問題的方法有好幾種,但是通過對比下來,最佳實踐為:
constructor(){
super();
this.handleClick = this.handleClick.bind(this);
}
不好的實踐為:
<Button onClick={this.handleClick.bind(this)}>按鈕</Button>
原因:上述代碼在render執(zhí)行的時候都會執(zhí)行一次,重復次數(shù)多,不像constructor只執(zhí)行一次。
9. 避免在類組件中使用箭頭函數(shù)創(chuàng)建類方法
在類組件中使用箭頭函數(shù)的好處就是完全不用擔心this的指向問題;因為箭頭函數(shù)并不會改變this的指向。但是我看到過一句話:this的指向問題從來就不是使用箭頭函數(shù)的原因。因此,并不推薦在類組件中使用箭頭函數(shù)創(chuàng)建方法。
從功利的角度來看,如果使用箭頭函數(shù)創(chuàng)建類的方法,此方法不會掛載在原型上,而是作為實例的一個屬性。也就是說如果此類組件實例化很多次,那么此方法也會被實例化相同次數(shù),這會造成極大的浪費。
因此,在類組件中解決this的最佳實踐仍然是在構(gòu)造函數(shù)中bind(this)
10. 避免使用內(nèi)聯(lián)樣式屬性
如果在項目中使用如下的代碼,那么在編譯之后,內(nèi)斂的style會被映射成為js代碼,最后就變成了js創(chuàng)建樣式,導致瀏覽器會花費更多的時間執(zhí)行腳本和渲染UI,從而降低了性能。
核心問題:CSS渲染UI的速度遠超過js,因此能不用js操作樣式就不要用!這一點很重要。本質(zhì)上還是js操作DOM很費時間。
不好的實踐:
fucntion App () {
return <div style={{backgroundColor: 'red'}}>div</div>
}
11. 對條件渲染進行優(yōu)化
這點主要是針對:頻繁的掛載和卸載組件是一項非常消耗性能的事情 這一事實提出的優(yōu)化,其本質(zhì)仍然是盡量減少對DOM的操作
好的實踐
function App() {
return (
<>
{true && <AdminHeader />}
<Header />
<Content />
</>
);
}
不好的實踐
function App() {
if (true) {
return (
<>
<AdminHeader />
<Header />
<Content />
</>
);
} else {
return (
<>
<Header />
<Content />
</>
);
}
}
第一種做法中,隨著條件的改變,重新渲染的只有AdminHeader組件,而第二種做法三個組件都會重新渲染,這也是由于虛擬DOM的對比策略所決定的。
12. 避免重復的無限渲染
避免在componentWillUpdate、componentDidUpdate或者render(是純函數(shù))方法中調(diào)用setState等可以觸發(fā)組件在此渲染的做法。本質(zhì)上是避免render函數(shù)循環(huán)調(diào)用自身。
13. 為組件創(chuàng)建錯誤邊界
先說不足:錯誤邊界本質(zhì)上是一個組件;但是只能在同步錯誤發(fā)生的時候顯示出來,異步錯誤是沒有辦法被錯誤邊界響應的!
錯誤邊界(Error Boundaries)是React的一個特性,它可以捕獲其子組件樹中JavaScript錯誤,記錄這些錯誤,并顯示備用UI,而不是讓整個組件樹崩潰。錯誤邊界只能通過類組件來實現(xiàn),因為需要使用生命周期方法componentDidCatch或getDerivedStateFromError。
以下分別介紹在類組件和函數(shù)式組件中如何處理錯誤,并舉例說明。
類組件中的錯誤邊界
在類組件中,你可以定義一個錯誤邊界組件,如下所示:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 當子組件拋出異常,這里將會被調(diào)用,返回新的state
return { hasError: true };
}
componentDidCatch(error, info) {
// 你同樣可以在這里記錄錯誤信息
console.error('ErrorBoundary caught an error', error, info);
}
render() {
if (this.state.hasError) {
// 當發(fā)生錯誤時,你可以渲染任何自定義的回退UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
然后你可以像這樣使用ErrorBoundary組件:
<ErrorBoundary> <MyComponent /> </ErrorBoundary>
這樣,如果MyComponent或者其任何子組件在渲染過程中發(fā)生JavaScript錯誤,ErrorBoundary就會顯示備用UI,并防止整個應用崩潰。
函數(shù)式組件中的錯誤處理
函數(shù)式組件不能直接創(chuàng)建錯誤邊界,因為它們不支持componentDidCatch或getDerivedStateFromError這類生命周期方法。然而,你可以在函數(shù)式組件內(nèi)使用hooks來處理錯誤,例如使用useState和useEffect來模擬類似的行為。不過,這不是標準的錯誤邊界實現(xiàn),標準的錯誤邊界目前只能通過類組件來實現(xiàn)。
但是,你可以通過將函數(shù)式組件包裹在上面定義的錯誤邊界類組件中來提供錯誤捕獲功能。
例如:
function MyFunctionalComponent() {
useEffect(() => {
try {
// 這里是可能會拋出錯誤的代碼
} catch (error) {
// 你可以在這里處理錯誤,例如設置狀態(tài)顯示錯誤信息
}
});
return (
// 你的組件返回值
);
}
// 應用錯誤邊界
<ErrorBoundary>
<MyFunctionalComponent />
</ErrorBoundary>
在這個例子中,任何在MyFunctionalComponent中發(fā)生的錯誤都需要自己處理,并不利用錯誤邊界來捕獲。但是被ErrorBoundary包裹的話,任何子組件樹中的錯誤仍然可以被ErrorBoundary捕獲。
總而言之,如果你希望在函數(shù)式組件中享有錯誤邊界的保護,你需要將函數(shù)式組件放入一個可以作為錯誤邊界的類組件之內(nèi)。 直到React提供函數(shù)式組件的官方錯誤邊界支持,這種方式將是常規(guī)的做法。
總結(jié)一下
本質(zhì)上還是:條件渲染;只不過引發(fā)條件變化的源在于:是否發(fā)生了錯誤!
14. 避免數(shù)據(jù)結(jié)構(gòu)的突變
結(jié)論:組件中的props和state的數(shù)據(jù)結(jié)構(gòu)應該保持一致,數(shù)據(jù)結(jié)構(gòu)的突變會導致輸出不一致??!這一點在state的層數(shù)比較深的時候一定要引起額外注意!
onClick={() =>
this.setState({
...this.state,
employee: {
...this.state.employee,
age: 30
}
})
}
15. 優(yōu)化依賴項大小
有一些庫不支持動態(tài)加載,比如說lodash。但是lodash提供了一些插件,使用這些插件也能夠?qū)崿F(xiàn)按需加載相同的效果,從而顯著的減少最終打包成的bundle的大小。
以上就是React中組件優(yōu)化的最佳方案分享的詳細內(nèi)容,更多關于React組件優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
React?Hooks useReducer?逃避deps組件渲染次數(shù)增加陷阱
這篇文章主要介紹了React?Hooks?之?useReducer?逃避deps后增加組件渲染次數(shù)的陷阱詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09
React+Typescript創(chuàng)建項目的實現(xiàn)步驟
通過React組件庫和TypeScript的強類型特性,開發(fā)者可以創(chuàng)建出具有優(yōu)秀用戶體驗和穩(wěn)定性的Web應用程序,本文主要介紹了React+Typescript創(chuàng)建項目的實現(xiàn)步驟,感興趣的可以了解一下2023-08-08
JS中使用react-tooltip插件實現(xiàn)鼠標懸浮顯示框
前段時間遇到的一個需求,要求鼠標懸停顯示使用描述, 用到了react-tooltip插件,今天寫一個總結(jié),感興趣的朋友跟隨小編一起看看吧2019-05-05
react echarts tooltip 區(qū)域新加輸入框編輯保存數(shù)據(jù)功能
這篇文章主要介紹了react echarts tooltip 區(qū)域新加輸入框編輯保存數(shù)據(jù)功能,大概思路是用一個div包裹echarts, 然后在echarts的同級新建一個div用來用來模擬真實tooltip,通過鼠標移入移出事件控制真實tooltip的顯示與隱藏,需要的朋友可以參考下2023-05-05
在react項目中webpack使用mock數(shù)據(jù)的操作方法
這篇文章主要介紹了在react項目中webpack使用mock數(shù)據(jù)的操作方法,本文給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-06-06

