本文主要讲解实战项目中React性能优化的方法,主要分为三个大的方面:减少不必要的组件更新、组件优化以及tree-shaking,共11个方法
一、减少不必要组件更新
以下是一些可以避免在 React 提交阶段进行不必要重新渲染的方法:
1、使用 React.memo
(对于函数组件)和 PureComponent
(对于类组件)
-
React.memo
:
React.memo
是一个高阶组件,用于包装函数组件。它通过对组件的props
进行浅层比较来决定是否重新渲染组件。示例:
javaimport React from 'react'; const MyComponent = React.memo(({ data }) => { // 组件渲染逻辑 return <div>{data}</div>; });
当
data
的引用没有发生变化时,组件将不会重新渲染。 -
PureComponent
(对于类组件):
PureComponent
会对props
和state
进行浅层比较。如果它们没有变化,组件将不会重新渲染。示例:
以下是一个在类组件中使用
PureComponent
的示例,包括数据传递和更新:
java
import React, { PureComponent } from 'react';
class MyComponent extends PureComponent {
// 构造函数,初始化状态
constructor(props) {
super(props);
this.state = {
count: 0,
name: 'Initial Name',
};
}
// 处理点击事件,更新状态
handleClick = () => {
// 示例 1:更新数字状态
this.setState({ count: this.state.count + 1 });
// 示例 2:更新字符串状态(如果 name 是从父组件传递的 props 且未变化,不会触发重新渲染)
// 假设 name 是从父组件传递的 props,以下更新不会触发重新渲染(如果 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;
在这个例子中:
-
MyComponent
是一个继承自PureComponent
的类组件。它有一个count
状态用于数字的递增展示,还有一个name
状态(也可以是从父组件传递的props
)用于展示字符串。 -
在
render
方法中,展示了count
和name
的值,并有一个按钮用于触发count
的递增。 -
ParentComponent
是父组件,它有一个name
状态,并将其传递给MyComponent
。还有一个按钮用于更改name
的状态。
PureComponent
会对 props
和 state
进行浅层比较。如果 props
或 state
的引用没有变化,组件将不会重新渲染。在上面的例子中,如果 MyComponent
接收到的 props.name
没有变化,并且 state
中的 count
没有更新,MyComponent
就不会重新渲染。
注意事项:
PureComponent
的浅层比较对于基本数据类型(如数字、字符串、布尔值)是有效的,但对于复杂数据类型(如对象、数组),它只会比较引用。如果对象或数组的内容发生变化,但引用不变,PureComponent
可能不会检测到变化。在这种情况下,可以使用immutable.js
或手动在shouldComponentUpdate
中进行深层比较。- 如果组件的
props
或state
变化频繁且计算成本不高,或者需要进行深层比较,可能不需要使用PureComponent
。
2、使用 useCallback
和 useMemo
-
useCallback
:
useCallback
用于记忆函数,确保传递给子组件的函数在依赖项不变的情况下不会重新创建。示例:
javaimport React, { useState, useCallback } from 'react'; function ParentComponent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { // 处理点击的逻辑 }, [count]); // 仅当 count 变化时重新创建函数 return ( <div> <ChildComponent onClick={handleClick} /> </div> ); }
-
useMemo
:
useMemo
用于记忆计算结果,避免在每次渲染时都进行昂贵的计算。示例:
javaimport React, { useState, useMemo } from 'react'; function MyComponent() { const [data, setData] = useState([]); const computedValue = useMemo(() => { // 进行昂贵的计算 return data.map((item) => item * 2); }, [data]); return <div>{computedValue}</div>; }
3、优化 shouldComponentUpdate
(对于类组件)
在类组件中,可以重写 shouldComponentUpdate
方法来进行更细粒度的控制。
java
import React from 'react';
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// 进行 props 和 state 的比较,决定是否更新
return (
nextProps.someValue!== this.props.someValue ||
nextState.someState!== this.state.someState
);
}
render() {
return <div>{/*... */}</div>;
}
}
4、避免在渲染阶段进行副作用操作
副作用操作(如网络请求、订阅事件等)应该在 useEffect
中进行,而不是在组件的渲染函数中。这样可以确保渲染函数的纯粹性,减少不必要的重新渲染触发。
java
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// 进行网络请求获取数据
fetchData().then((result) => setData(result));
}, []); // 空依赖数组确保只在组件挂载时执行一次
return <div>{data? data : 'Loading...'}</div>;
}
5、正确设置 key
属性(对于列表渲染)
-
在渲染列表时,为每个列表项设置唯一的
key
属性。这有助于 React 更高效地识别和更新列表项。javaimport React from 'react'; function ListComponent({ items }) { return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }
二、组件优化
1、useIntersectionObserver
在 React 项目中使用 TypeScript 和 useIntersectionObserver
实现虚拟滚动懒加载的示例代码:
java
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) {
// 这里可以进行实际的图片加载或其他数据加载逻辑
const index = imageRefs.current.findIndex((ref) => ref === entry.target);
console.log(`图片 ${index + 1} 进入可视区域`);
// 加载完成后可以停止观察该元素
observerRef.current?.unobserve(entry.target);
}
});
}, options);
// 开始观察所有的元素
imageRefs.current.forEach((ref) => {
if (ref) {
observerRef.current?.observe(ref);
}
});
return () => {
// 组件卸载时清理观察者
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) => (
<div
key={index}
ref={(ref) => {
imageRefs.current[index] = ref as HTMLDivElement;
}}
style={{
height: '200px',
width: '200px',
backgroundColor: 'gray',
marginBottom: '10px',
}}
/>
))}
</div>
);
}
export default LazyLoadComponent;
示例详述
useRef
用于创建imageRefs
和observerRef
引用,imageRefs
用于存储每个元素的引用,observerRef
用于存储IntersectionObserver
的实例。useEffect
中创建了IntersectionObserver
实例,并设置了观察的选项。在entries
的回调中,当元素进入可视区域时进行相应的操作,这里只是简单地打印了信息。- 在返回的组件结构中,模拟了一个包含多个灰色方块的列表,每个方块都有一个
ref
,用于被观察。
注意,实际应用中,你需要根据具体的需求进行更多的逻辑处理和样式调整,比如实际的图片加载、数据获取等操作。
2、react-lazyload
在 React 项目中,react-lazyload
可以用于长列表加载。
(一)基本原理和适用场景
react-lazyload
的核心原理是监听元素是否进入可视区域,当元素进入可视区域时才触发实际的加载操作。对于长列表加载场景,这一特性非常有用。
在长列表中,可能存在大量的数据项需要展示,一次性加载所有数据项可能会导致性能问题,尤其是在处理图片等资源较大的内容时。使用 react-lazyload
可以延迟加载列表中的元素,只有当用户滚动到相应位置,元素即将进入可视区域时才进行加载,这样可以显著提高初始页面加载速度和整体的用户体验。
(二)使用示例
以下是一个在 React 项目中使用 react-lazyload
处理长列表加载的简单示例:
-
首先,安装
react-lazyload
:bashnpm install react-lazyload
-
然后在代码中使用:
java
import React from 'react';
import LazyLoad from 'react-lazyload';
import './App.css';
const ListItem = ({ index }) => (
<div style={{ height: 100, backgroundColor: 'lightblue', marginBottom: 10 }}>
列表项 {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;
在上述示例中,创建了一个包含 100 个列表项的长列表,通过 react-lazyload
的 LazyLoad
组件包裹每个列表项,实现了懒加载功能。当用户滚动列表时,每个列表项会根据其是否进入可视区域来决定是否进行加载。
(三)性能优势
-
减少初始加载时间:在长列表场景下,不必在页面初始加载时就加载所有的列表项内容,尤其是当列表项包含较大的图片或其他资源时,这可以大大减少初始页面加载时间,让用户更快地看到页面的主要内容。
-
降低内存占用:由于不是一次性加载所有数据,因此可以减少内存的占用,特别是对于移动设备或内存有限的环境,这有助于提高设备的响应速度和整体性能。
-
优化用户体验:通过逐步加载内容,避免了因为大量数据同时加载而导致的页面卡顿或无响应现象,用户可以在滚动过程中平滑地浏览列表内容,提升了用户体验。
(四)注意事项
样式处理 :在使用 react-lazyload
时,需要注意列表项的样式设置。特别是当列表项的高度或宽度不确定时,可能会导致懒加载的判断出现偏差。可以通过固定列表项的尺寸或者使用合适的 CSS 布局技巧来解决这个问题。
三、tree-shaking
1、package.json
中的 sideEffects
配置
- 在
package.json
中添加"sideEffects"
字段:
如果你的项目中所有的.css
文件都没有副作用(例如没有在 CSS 中使用:global
或类似会产生全局影响的选择器),可以将"sideEffects"
配置为false
,这将告诉 Webpack 可以更激进地进行 Tree Shaking。
json
{
"name": "your-app",
"version": "1.0.0",
"sideEffects": false
}
如果项目中有部分文件有副作用,你可以这样配置:
json
{
"name": "your-app",
"version": "1.0.0",
"sideEffects": [
"*.css",
"some-module-with-side-effects"
]
}
这里列出了有副作用的文件或模块,其他未列出的模块将被更积极地进行 Tree Shaking
。
2、组件按需加载Babel-plugin-import
以下是一个在 React 项目中使用 Babel-plugin-import
的代码示例。
-
首先创建一个简单的 React 项目结构:
my-react-app/ ├── package.json ├── src/ │ ├── App.js │ └── index.js
-
在
package.json
中添加必要的依赖: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" } }
-
创建
.babelrc
文件并配置Babel-plugin-import
:json{ "presets": [ "@babel/preset-react", "@babel/preset-env" ], "plugins": [ [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" } ] ] }
-
在
src/App.js
中编写示例代码:jsximport React from 'react'; // 使用 Babel-plugin-import 优化引入 antd 的 Button 组件 import { Button } from 'antd'; const App = () => { return ( <div> <Button type="primary">点击我</Button> </div> ); }; export default App;
-
在
src/index.js
中渲染App
组件:javaimport React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));
-
假设使用 Webpack 进行构建,配置
webpack.config.js
:javascriptconst 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' } } ] } };
这样,在项目中通过 Babel-plugin-import
对 antd
的组件引入进行了优化,实际应用中可以根据自己的项目需求和库的使用情况进行相应的调整。
3、使用 Lodash 库的优化
以下是一个简单的代码示例,展示如何在 React 项目中使用 lodash-es
版本并结合 Webpack 的 Tree Shaking 功能:
-
创建一个 React 项目:
bashnpx create-react-app my-lodash-example cd my-lodash-example
-
安装
lodash-es
:bashnpm install lodash-es
-
创建一个示例组件
App.js
:javaimport 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
(如果你的项目没有真正的副作用):json{ "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" } }
-
因为
create-react-app
隐藏了 Webpack 配置,但是在生产构建模式下(npm run build
),它默认会启用 Tree Shaking。
在这个示例中,我们只从 lodash-es
中引入了 pick
函数,并且通过配置 sideEffects
和在生产构建时,Webpack 会进行 Tree Shaking 来去除未使用的代码。
在 create-react-app
项目中,虽然隐藏了 Webpack 配置,但默认在生产构建时已经开启了一些优化措施包括 Tree Shaking,不过你可以通过以下几种方式来进一步优化和确保 Tree Shaking 效果:
4、使用 purgecss
(针对 CSS)
-
安装
purgecss
及其相关依赖:bashnpm install purgecss purgecss-webpack-plugin --save-dev
-
在
webpack.config.js
(虽然create-react-app
隐藏了此文件,但可以通过eject
暴露出来,这是一个不可逆操作,需谨慎考虑)中添加PurgeCSSPlugin
:javascriptconst PurgeCSSPlugin = require('purgecss-webpack-plugin'); module.exports = { //...其他配置 plugins: [ new PurgeCSSPlugin({ paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }), }), ], };
这将帮助去除未使用的 CSS 代码,与 Tree Shaking 一起优化项目体积。
请注意,在对 create-react-app
的配置进行修改时,尤其是涉及到 eject
操作,要充分了解其影响和风险,并且在修改前最好备份项目代码。