想象力做網(wǎng)站百度官方推廣平臺(tái)
本文主要講解實(shí)戰(zhàn)項(xiàng)目中React性能優(yōu)化的方法,主要分為三個(gè)大的方面:減少不必要的組件更新、組件優(yōu)化以及tree-shaking,共11個(gè)方法
一、減少不必要組件更新
以下是一些可以避免在 React 提交階段進(jìn)行不必要重新渲染的方法:
1、使用 React.memo
(對(duì)于函數(shù)組件)和 PureComponent
(對(duì)于類組件)
-
React.memo
:
React.memo
是一個(gè)高階組件,用于包裝函數(shù)組件。它通過(guò)對(duì)組件的props
進(jìn)行淺層比較來(lái)決定是否重新渲染組件。
示例:import React from 'react';const MyComponent = React.memo(({ data }) => {// 組件渲染邏輯return <div>{data}</div>; });
當(dāng)
data
的引用沒(méi)有發(fā)生變化時(shí),組件將不會(huì)重新渲染。 -
PureComponent
(對(duì)于類組件):
PureComponent
會(huì)對(duì)props
和state
進(jìn)行淺層比較。如果它們沒(méi)有變化,組件將不會(huì)重新渲染。
示例:
以下是一個(gè)在類組件中使用PureComponent
的示例,包括數(shù)據(jù)傳遞和更新:
import React, { PureComponent } from 'react';class MyComponent extends PureComponent {// 構(gòu)造函數(shù),初始化狀態(tài)constructor(props) {super(props);this.state = {count: 0,name: 'Initial Name',};}// 處理點(diǎn)擊事件,更新?tīng)顟B(tài)handleClick = () => {// 示例 1:更新數(shù)字狀態(tài)this.setState({ count: this.state.count + 1 });// 示例 2:更新字符串狀態(tài)(如果 name 是從父組件傳遞的 props 且未變化,不會(huì)觸發(fā)重新渲染)// 假設(shè) name 是從父組件傳遞的 props,以下更新不會(huì)觸發(fā)重新渲染(如果 name 未變化)// this.setState({ name: this.props.name });};render() {return (<div><p>Count: {this.state.count}</p><p>Name: {this.state.name}</p><button onClick={this.handleClick}>Increment Count</button></div>);}
}// 父組件
class ParentComponent extends React.Component {constructor(props) {super(props);this.state = {name: 'Parent Name',};}handleNameChange = () => {this.setState({ name: 'Updated Name' });};render() {return (<div><MyComponent name={this.state.name} /><button onClick={this.handleNameChange}>Change Name</button></div>);}
}export default ParentComponent;
在這個(gè)例子中:
-
MyComponent
是一個(gè)繼承自PureComponent
的類組件。它有一個(gè)count
狀態(tài)用于數(shù)字的遞增展示,還有一個(gè)name
狀態(tài)(也可以是從父組件傳遞的props
)用于展示字符串。 -
在
render
方法中,展示了count
和name
的值,并有一個(gè)按鈕用于觸發(fā)count
的遞增。 -
ParentComponent
是父組件,它有一個(gè)name
狀態(tài),并將其傳遞給MyComponent
。還有一個(gè)按鈕用于更改name
的狀態(tài)。
PureComponent
會(huì)對(duì) props
和 state
進(jìn)行淺層比較。如果 props
或 state
的引用沒(méi)有變化,組件將不會(huì)重新渲染。在上面的例子中,如果 MyComponent
接收到的 props.name
沒(méi)有變化,并且 state
中的 count
沒(méi)有更新,MyComponent
就不會(huì)重新渲染。
注意事項(xiàng):
PureComponent
的淺層比較對(duì)于基本數(shù)據(jù)類型(如數(shù)字、字符串、布爾值)是有效的,但對(duì)于復(fù)雜數(shù)據(jù)類型(如對(duì)象、數(shù)組),它只會(huì)比較引用。如果對(duì)象或數(shù)組的內(nèi)容發(fā)生變化,但引用不變,PureComponent
可能不會(huì)檢測(cè)到變化。在這種情況下,可以使用immutable.js
或手動(dòng)在shouldComponentUpdate
中進(jìn)行深層比較。- 如果組件的
props
或state
變化頻繁且計(jì)算成本不高,或者需要進(jìn)行深層比較,可能不需要使用PureComponent
。
2、使用 useCallback
和 useMemo
-
useCallback
:
useCallback
用于記憶函數(shù),確保傳遞給子組件的函數(shù)在依賴項(xiàng)不變的情況下不會(huì)重新創(chuàng)建。
示例:import React, { useState, useCallback } from 'react';function ParentComponent() {const [count, setCount] = useState(0);const handleClick = useCallback(() => {// 處理點(diǎn)擊的邏輯}, [count]); // 僅當(dāng) count 變化時(shí)重新創(chuàng)建函數(shù)return (<div><ChildComponent onClick={handleClick} /></div>); }
-
useMemo
:
useMemo
用于記憶計(jì)算結(jié)果,避免在每次渲染時(shí)都進(jìn)行昂貴的計(jì)算。
示例:import React, { useState, useMemo } from 'react';function MyComponent() {const [data, setData] = useState([]);const computedValue = useMemo(() => {// 進(jìn)行昂貴的計(jì)算return data.map((item) => item * 2);}, [data]);return <div>{computedValue}</div>; }
3、優(yōu)化 shouldComponentUpdate
(對(duì)于類組件)
在類組件中,可以重寫 shouldComponentUpdate
方法來(lái)進(jìn)行更細(xì)粒度的控制。
import React from 'react';class MyComponent extends React.Component {shouldComponentUpdate(nextProps, nextState) {// 進(jìn)行 props 和 state 的比較,決定是否更新return (nextProps.someValue!== this.props.someValue ||nextState.someState!== this.state.someState);}render() {return <div>{/*... */}</div>;}
}
4、避免在渲染階段進(jìn)行副作用操作
副作用操作(如網(wǎng)絡(luò)請(qǐng)求、訂閱事件等)應(yīng)該在 useEffect
中進(jìn)行,而不是在組件的渲染函數(shù)中。這樣可以確保渲染函數(shù)的純粹性,減少不必要的重新渲染觸發(fā)。
import React, { useState, useEffect } from 'react';function MyComponent() {const [data, setData] = useState(null);useEffect(() => {// 進(jìn)行網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)fetchData().then((result) => setData(result));}, []); // 空依賴數(shù)組確保只在組件掛載時(shí)執(zhí)行一次return <div>{data? data : 'Loading...'}</div>;
}
5、正確設(shè)置 key
屬性(對(duì)于列表渲染)
- 在渲染列表時(shí),為每個(gè)列表項(xiàng)設(shè)置唯一的
key
屬性。這有助于 React 更高效地識(shí)別和更新列表項(xiàng)。import React from 'react';function ListComponent({ items }) {return (<ul>{items.map((item) => (<li key={item.id}>{item.name}</li>))}</ul>); }
二、組件優(yōu)化
1、useIntersectionObserver
在 React 項(xiàng)目中使用 TypeScript 和 useIntersectionObserver
實(shí)現(xiàn)虛擬滾動(dòng)懶加載的示例代碼:
import React, { useEffect, useRef } from 'react';function LazyLoadComponent() {const imageRefs = useRef<HTMLDivElement[]>([]);const observerRef = useRef<IntersectionObserver | null>(null);useEffect(() => {const options = {root: null,rootMargin: '0px',threshold: 0.1,};observerRef.current = new IntersectionObserver((entries) => {entries.forEach((entry) => {if (entry.isIntersecting) {// 這里可以進(jìn)行實(shí)際的圖片加載或其他數(shù)據(jù)加載邏輯const index = imageRefs.current.findIndex((ref) => ref === entry.target);console.log(`圖片 ${index + 1} 進(jìn)入可視區(qū)域`);// 加載完成后可以停止觀察該元素observerRef.current?.unobserve(entry.target);}});}, options);// 開(kāi)始觀察所有的元素imageRefs.current.forEach((ref) => {if (ref) {observerRef.current?.observe(ref);}});return () => {// 組件卸載時(shí)清理觀察者if (observerRef.current) {observerRef.current.disconnect();}};}, []);const imageList = Array.from({ length: 10 }, (_, index) => index + 1);return (<div style={{ height: '300px', overflowY: 'auto' }}>{imageList.map((item, index) => (<divkey={index}ref={(ref) => {imageRefs.current[index] = ref as HTMLDivElement;}}style={{height: '200px',width: '200px',backgroundColor: 'gray',marginBottom: '10px',}}/>))}</div>);
}export default LazyLoadComponent;
示例詳述
useRef
用于創(chuàng)建imageRefs
和observerRef
引用,imageRefs
用于存儲(chǔ)每個(gè)元素的引用,observerRef
用于存儲(chǔ)IntersectionObserver
的實(shí)例。useEffect
中創(chuàng)建了IntersectionObserver
實(shí)例,并設(shè)置了觀察的選項(xiàng)。在entries
的回調(diào)中,當(dāng)元素進(jìn)入可視區(qū)域時(shí)進(jìn)行相應(yīng)的操作,這里只是簡(jiǎn)單地打印了信息。- 在返回的組件結(jié)構(gòu)中,模擬了一個(gè)包含多個(gè)灰色方塊的列表,每個(gè)方塊都有一個(gè)
ref
,用于被觀察。
注意,實(shí)際應(yīng)用中,你需要根據(jù)具體的需求進(jìn)行更多的邏輯處理和樣式調(diào)整,比如實(shí)際的圖片加載、數(shù)據(jù)獲取等操作。
2、react-lazyload
在 React 項(xiàng)目中,react-lazyload
可以用于長(zhǎng)列表加載。
(一)基本原理和適用場(chǎng)景
react-lazyload
的核心原理是監(jiān)聽(tīng)元素是否進(jìn)入可視區(qū)域,當(dāng)元素進(jìn)入可視區(qū)域時(shí)才觸發(fā)實(shí)際的加載操作。對(duì)于長(zhǎng)列表加載場(chǎng)景,這一特性非常有用。
在長(zhǎng)列表中,可能存在大量的數(shù)據(jù)項(xiàng)需要展示,一次性加載所有數(shù)據(jù)項(xiàng)可能會(huì)導(dǎo)致性能問(wèn)題,尤其是在處理圖片等資源較大的內(nèi)容時(shí)。使用 react-lazyload
可以延遲加載列表中的元素,只有當(dāng)用戶滾動(dòng)到相應(yīng)位置,元素即將進(jìn)入可視區(qū)域時(shí)才進(jìn)行加載,這樣可以顯著提高初始頁(yè)面加載速度和整體的用戶體驗(yàn)。
(二)使用示例
以下是一個(gè)在 React 項(xiàng)目中使用 react-lazyload
處理長(zhǎng)列表加載的簡(jiǎn)單示例:
-
首先,安裝
react-lazyload
:npm install react-lazyload
-
然后在代碼中使用:
import React from 'react';
import LazyLoad from 'react-lazyload';
import './App.css';const ListItem = ({ index }) => (<div style={{ height: 100, backgroundColor: 'lightblue', marginBottom: 10 }}>列表項(xiàng) {index}</div>
);const LongList = () => {const listLength = 100;const listItems = [];for (let i = 0; i < listLength; i++) {listItems.push(<ListItem key={i} index={i} />);}return (<div style={{ height: 500, overflowY: 'scroll' }}>{listItems.map((item, index) => (<LazyLoad key={index} once={true}>{item}</LazyLoad>))}</div>);
};export default LongList;
在上述示例中,創(chuàng)建了一個(gè)包含 100 個(gè)列表項(xiàng)的長(zhǎng)列表,通過(guò) react-lazyload
的 LazyLoad
組件包裹每個(gè)列表項(xiàng),實(shí)現(xiàn)了懶加載功能。當(dāng)用戶滾動(dòng)列表時(shí),每個(gè)列表項(xiàng)會(huì)根據(jù)其是否進(jìn)入可視區(qū)域來(lái)決定是否進(jìn)行加載。
(三)性能優(yōu)勢(shì)
-
減少初始加載時(shí)間:在長(zhǎng)列表場(chǎng)景下,不必在頁(yè)面初始加載時(shí)就加載所有的列表項(xiàng)內(nèi)容,尤其是當(dāng)列表項(xiàng)包含較大的圖片或其他資源時(shí),這可以大大減少初始頁(yè)面加載時(shí)間,讓用戶更快地看到頁(yè)面的主要內(nèi)容。
-
降低內(nèi)存占用:由于不是一次性加載所有數(shù)據(jù),因此可以減少內(nèi)存的占用,特別是對(duì)于移動(dòng)設(shè)備或內(nèi)存有限的環(huán)境,這有助于提高設(shè)備的響應(yīng)速度和整體性能。
-
優(yōu)化用戶體驗(yàn):通過(guò)逐步加載內(nèi)容,避免了因?yàn)榇罅繑?shù)據(jù)同時(shí)加載而導(dǎo)致的頁(yè)面卡頓或無(wú)響應(yīng)現(xiàn)象,用戶可以在滾動(dòng)過(guò)程中平滑地瀏覽列表內(nèi)容,提升了用戶體驗(yàn)。
(四)注意事項(xiàng)
樣式處理:在使用 react-lazyload
時(shí),需要注意列表項(xiàng)的樣式設(shè)置。特別是當(dāng)列表項(xiàng)的高度或?qū)挾炔淮_定時(shí),可能會(huì)導(dǎo)致懶加載的判斷出現(xiàn)偏差??梢酝ㄟ^(guò)固定列表項(xiàng)的尺寸或者使用合適的 CSS 布局技巧來(lái)解決這個(gè)問(wèn)題。
三、tree-shaking
1、package.json
中的 sideEffects
配置
- 在
package.json
中添加"sideEffects"
字段:
如果你的項(xiàng)目中所有的.css
文件都沒(méi)有副作用(例如沒(méi)有在 CSS 中使用:global
或類似會(huì)產(chǎn)生全局影響的選擇器),可以將"sideEffects"
配置為false
,這將告訴 Webpack 可以更激進(jìn)地進(jìn)行 Tree Shaking。
{"name": "your-app","version": "1.0.0","sideEffects": false}
如果項(xiàng)目中有部分文件有副作用,你可以這樣配置:
{"name": "your-app","version": "1.0.0","sideEffects": ["*.css","some-module-with-side-effects"]
}
這里列出了有副作用的文件或模塊,其他未列出的模塊將被更積極地進(jìn)行 Tree Shaking
。
2、組件按需加載Babel-plugin-import
以下是一個(gè)在 React 項(xiàng)目中使用 Babel-plugin-import
的代碼示例。
-
首先創(chuàng)建一個(gè)簡(jiǎn)單的 React 項(xiàng)目結(jié)構(gòu):
my-react-app/ ├── package.json ├── src/ │ ├── App.js │ └── index.js
-
在
package.json
中添加必要的依賴:{"dependencies": {"react": "^18.2.0","react-dom": "^18.2.0"},"devDependencies": {"@babel/core": "^7.22.10","@babel/plugin-proposal-class-properties": "^7.22.3","@babel/plugin-transform-runtime": "^7.22.5","@babel/preset-env": "^7.22.5","@babel/preset-react": "^7.18.6","babel-loader": "^9.1.2"} }
-
創(chuàng)建
.babelrc
文件并配置Babel-plugin-import
:{"presets": ["@babel/preset-react","@babel/preset-env"],"plugins": [["import",{"libraryName": "antd","libraryDirectory": "es","style": "css"}]] }
-
在
src/App.js
中編寫示例代碼:import React from 'react'; // 使用 Babel-plugin-import 優(yōu)化引入 antd 的 Button 組件 import { Button } from 'antd';const App = () => {return (<div><Button type="primary">點(diǎn)擊我</Button></div>); };export default App;
-
在
src/index.js
中渲染App
組件:import React from 'react'; import ReactDOM from 'react-dom'; import App from './App';ReactDOM.render(<App />, document.getElementById('root'));
-
假設(shè)使用 Webpack 進(jìn)行構(gòu)建,配置
webpack.config.js
:const path = require('path');module.exports = {entry: './src/index.js',output: {path: path.resolve(__dirname, 'dist'),filename: 'bundle.js'},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: {loader: 'babel-loader'}}]} };
這樣,在項(xiàng)目中通過(guò) Babel-plugin-import
對(duì) antd
的組件引入進(jìn)行了優(yōu)化,實(shí)際應(yīng)用中可以根據(jù)自己的項(xiàng)目需求和庫(kù)的使用情況進(jìn)行相應(yīng)的調(diào)整。
3、使用 Lodash 庫(kù)的優(yōu)化
以下是一個(gè)簡(jiǎn)單的代碼示例,展示如何在 React 項(xiàng)目中使用 lodash-es
版本并結(jié)合 Webpack 的 Tree Shaking 功能:
-
創(chuàng)建一個(gè) React 項(xiàng)目:
npx create-react-app my-lodash-example cd my-lodash-example
-
安裝
lodash-es
:npm install lodash-es
-
創(chuàng)建一個(gè)示例組件
App.js
:import React from 'react'; import pick from 'lodash-es/pick';const data = {name: 'John',age: 30,city: 'New York' };const filteredData = pick(data, ['name', 'age']);const App = () => {return (<div><p>Name: {filteredData.name}</p><p>Age: {filteredData.age}</p></div>); };export default App;
-
在
package.json
中確保"sideEffects": false
(如果你的項(xiàng)目沒(méi)有真正的副作用):{"name": "my-lodash-example","version": "0.1.0","private": true,"dependencies": {//..."lodash-es": "^4.17.21","react": "^18.2.0","react-dom": "^18.2.0","react-scripts": "5.0.1"},"sideEffects": false,"scripts": {"start": "react-scripts start","build": "react-scripts build","test": "react-scripts test","eject": "react-scripts eject"} }
-
因?yàn)?
create-react-app
隱藏了 Webpack 配置,但是在生產(chǎn)構(gòu)建模式下(npm run build
),它默認(rèn)會(huì)啟用 Tree Shaking。
在這個(gè)示例中,我們只從 lodash-es
中引入了 pick
函數(shù),并且通過(guò)配置 sideEffects
和在生產(chǎn)構(gòu)建時(shí),Webpack 會(huì)進(jìn)行 Tree Shaking 來(lái)去除未使用的代碼。
在 create-react-app
項(xiàng)目中,雖然隱藏了 Webpack 配置,但默認(rèn)在生產(chǎn)構(gòu)建時(shí)已經(jīng)開(kāi)啟了一些優(yōu)化措施包括 Tree Shaking,不過(guò)你可以通過(guò)以下幾種方式來(lái)進(jìn)一步優(yōu)化和確保 Tree Shaking 效果:
4、使用 purgecss
(針對(duì) CSS)
-
安裝
purgecss
及其相關(guān)依賴:npm install purgecss purgecss-webpack-plugin --save-dev
-
在
webpack.config.js
(雖然create-react-app
隱藏了此文件,但可以通過(guò)eject
暴露出來(lái),這是一個(gè)不可逆操作,需謹(jǐn)慎考慮)中添加PurgeCSSPlugin
:const PurgeCSSPlugin = require('purgecss-webpack-plugin');module.exports = {//...其他配置plugins: [new PurgeCSSPlugin({paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),}),], };
這將幫助去除未使用的 CSS 代碼,與 Tree Shaking 一起優(yōu)化項(xiàng)目體積。
請(qǐng)注意,在對(duì) create-react-app
的配置進(jìn)行修改時(shí),尤其是涉及到 eject
操作,要充分了解其影響和風(fēng)險(xiǎn),并且在修改前最好備份項(xiàng)目代碼。