大厂进阶八:React性能优化精华篇

本文主要讲解实战项目中React性能优化的方法,主要分为三个大的方面:减少不必要的组件更新、组件优化以及tree-shaking,共11个方法

一、减少不必要组件更新

以下是一些可以避免在 React 提交阶段进行不必要重新渲染的方法:

1、使用 React.memo(对于函数组件)和 PureComponent(对于类组件)

  1. React.memo
    React.memo 是一个高阶组件,用于包装函数组件。它通过对组件的 props 进行浅层比较来决定是否重新渲染组件。

    示例:

    java 复制代码
    import React from 'react';
    
    const MyComponent = React.memo(({ data }) => {
        // 组件渲染逻辑
        return <div>{data}</div>;
    });

    data 的引用没有发生变化时,组件将不会重新渲染。

  2. PureComponent(对于类组件):
    PureComponent 会对 propsstate 进行浅层比较。如果它们没有变化,组件将不会重新渲染。

    示例:

    以下是一个在类组件中使用 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 方法中,展示了 countname 的值,并有一个按钮用于触发 count 的递增。

  • ParentComponent 是父组件,它有一个 name 状态,并将其传递给 MyComponent。还有一个按钮用于更改 name 的状态。

PureComponent 会对 propsstate 进行浅层比较。如果 propsstate 的引用没有变化,组件将不会重新渲染。在上面的例子中,如果 MyComponent 接收到的 props.name 没有变化,并且 state 中的 count 没有更新,MyComponent 就不会重新渲染。

注意事项:

  • PureComponent 的浅层比较对于基本数据类型(如数字、字符串、布尔值)是有效的,但对于复杂数据类型(如对象、数组),它只会比较引用。如果对象或数组的内容发生变化,但引用不变,PureComponent 可能不会检测到变化。在这种情况下,可以使用 immutable.js 或手动在 shouldComponentUpdate 中进行深层比较。
  • 如果组件的 propsstate 变化频繁且计算成本不高,或者需要进行深层比较,可能不需要使用 PureComponent

2、使用 useCallbackuseMemo

  1. useCallback
    useCallback 用于记忆函数,确保传递给子组件的函数在依赖项不变的情况下不会重新创建。

    示例:

    java 复制代码
    import React, { useState, useCallback } from 'react';
    
    function ParentComponent() {
        const [count, setCount] = useState(0);
        const handleClick = useCallback(() => {
            // 处理点击的逻辑
        }, [count]); // 仅当 count 变化时重新创建函数
    
        return (
            <div>
                <ChildComponent onClick={handleClick} />
            </div>
        );
    }
  2. useMemo
    useMemo 用于记忆计算结果,避免在每次渲染时都进行昂贵的计算。

    示例:

    java 复制代码
    import 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 更高效地识别和更新列表项。

    java 复制代码
    import 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 用于创建 imageRefsobserverRef 引用,imageRefs 用于存储每个元素的引用,observerRef 用于存储 IntersectionObserver 的实例。
  • useEffect 中创建了 IntersectionObserver 实例,并设置了观察的选项。在 entries 的回调中,当元素进入可视区域时进行相应的操作,这里只是简单地打印了信息。
  • 在返回的组件结构中,模拟了一个包含多个灰色方块的列表,每个方块都有一个 ref,用于被观察。

注意,实际应用中,你需要根据具体的需求进行更多的逻辑处理和样式调整,比如实际的图片加载、数据获取等操作。

2、react-lazyload

在 React 项目中,react-lazyload 可以用于长列表加载。

(一)基本原理和适用场景

react-lazyload 的核心原理是监听元素是否进入可视区域,当元素进入可视区域时才触发实际的加载操作。对于长列表加载场景,这一特性非常有用。

在长列表中,可能存在大量的数据项需要展示,一次性加载所有数据项可能会导致性能问题,尤其是在处理图片等资源较大的内容时。使用 react-lazyload 可以延迟加载列表中的元素,只有当用户滚动到相应位置,元素即将进入可视区域时才进行加载,这样可以显著提高初始页面加载速度和整体的用户体验。

(二)使用示例

以下是一个在 React 项目中使用 react-lazyload 处理长列表加载的简单示例:

  1. 首先,安装 react-lazyload

    bash 复制代码
    npm install react-lazyload
  2. 然后在代码中使用:

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-lazyloadLazyLoad 组件包裹每个列表项,实现了懒加载功能。当用户滚动列表时,每个列表项会根据其是否进入可视区域来决定是否进行加载。

(三)性能优势

  • 减少初始加载时间:在长列表场景下,不必在页面初始加载时就加载所有的列表项内容,尤其是当列表项包含较大的图片或其他资源时,这可以大大减少初始页面加载时间,让用户更快地看到页面的主要内容。

  • 降低内存占用:由于不是一次性加载所有数据,因此可以减少内存的占用,特别是对于移动设备或内存有限的环境,这有助于提高设备的响应速度和整体性能。

  • 优化用户体验:通过逐步加载内容,避免了因为大量数据同时加载而导致的页面卡顿或无响应现象,用户可以在滚动过程中平滑地浏览列表内容,提升了用户体验。

(四)注意事项

样式处理 :在使用 react-lazyload 时,需要注意列表项的样式设置。特别是当列表项的高度或宽度不确定时,可能会导致懒加载的判断出现偏差。可以通过固定列表项的尺寸或者使用合适的 CSS 布局技巧来解决这个问题。

三、tree-shaking

1、package.json 中的 sideEffects 配置

  1. 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 的代码示例。

  1. 首先创建一个简单的 React 项目结构:

    复制代码
    my-react-app/
    ├── package.json
    ├── src/
    │   ├── App.js
    │   └── index.js
  2. 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"
      }
    }
  3. 创建 .babelrc 文件并配置 Babel-plugin-import

    json 复制代码
    {
      "presets": [
        "@babel/preset-react",
        "@babel/preset-env"
      ],
      "plugins": [
        [
          "import",
          {
            "libraryName": "antd",
            "libraryDirectory": "es",
            "style": "css"
          }
        ]
      ]
    }
  4. src/App.js 中编写示例代码:

    jsx 复制代码
    import React from 'react';
    // 使用 Babel-plugin-import 优化引入 antd 的 Button 组件
    import { Button } from 'antd';
    
    const App = () => {
      return (
        <div>
          <Button type="primary">点击我</Button>
        </div>
      );
    };
    
    export default App;
  5. src/index.js 中渲染 App 组件:

    java 复制代码
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    
    ReactDOM.render(<App />, document.getElementById('root'));
  6. 假设使用 Webpack 进行构建,配置 webpack.config.js

    javascript 复制代码
    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'
            }
          }
        ]
      }
    };

这样,在项目中通过 Babel-plugin-importantd 的组件引入进行了优化,实际应用中可以根据自己的项目需求和库的使用情况进行相应的调整。

3、使用 Lodash 库的优化

以下是一个简单的代码示例,展示如何在 React 项目中使用 lodash-es 版本并结合 Webpack 的 Tree Shaking 功能:

  1. 创建一个 React 项目:

    bash 复制代码
    npx create-react-app my-lodash-example
    cd my-lodash-example
  2. 安装 lodash-es

    bash 复制代码
    npm install lodash-es
  3. 创建一个示例组件 App.js

    java 复制代码
    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;
  4. 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"
      }
    }
  5. 因为 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)

  1. 安装 purgecss 及其相关依赖:

    bash 复制代码
    npm install purgecss purgecss-webpack-plugin --save-dev
  2. webpack.config.js(虽然 create-react-app 隐藏了此文件,但可以通过 eject 暴露出来,这是一个不可逆操作,需谨慎考虑)中添加 PurgeCSSPlugin

    javascript 复制代码
    const PurgeCSSPlugin = require('purgecss-webpack-plugin');
    
    module.exports = {
      //...其他配置
      plugins: [
        new PurgeCSSPlugin({
          paths: glob.sync(`${paths.appSrc}/**/*`, { nodir: true }),
        }),
      ],
    };

    这将帮助去除未使用的 CSS 代码,与 Tree Shaking 一起优化项目体积。

请注意,在对 create-react-app 的配置进行修改时,尤其是涉及到 eject 操作,要充分了解其影响和风险,并且在修改前最好备份项目代码。

相关推荐
万少5 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL11 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl0227 分钟前
java web5(黑马)
java·开发语言·前端
Amy.Wang28 分钟前
前端如何实现电子签名
前端·javascript·html5
海天胜景30 分钟前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼31 分钟前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿33 分钟前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再35 分钟前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling55539 分钟前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架
拾光拾趣录44 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css