1.React项目如何进行性能优化 #

2.编译阶段的优化 #

2.1. 初始化项目 #

npm  init -y
npm install --save react react-dom react-router-dom lodash bootstrap  is-array reselect redux

npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin  babel-loader @babel/core @babel/preset-env @babel/preset-react style-loader css-loader postcss-loader @babel/plugin-syntax-class-properties mini-css-extract-plugin 
作用
react React 是一个用于构建用户界面的 JavaScript 库
react-dom 使 React 能够与 DOM 交互
lodash 一个 JavaScript 实用程序库,提供了很多有用的工具函数
bootstrap 一种流行的前端框架,用于开发响应式网站和应用程序
is-array 一个小的实用函数,用于检查一个值是否是数组
reselect 用于创建可记忆的、可组合的 selector 函数
redux 一个用于管理应用状态的 JavaScript 库
开发依赖包 作用
webpack 用于打包 JavaScript 文件、CSS、图片等资源的模块打包器
webpack-cli 用于在命令行中运行 webpack
webpack-dev-server 用于快速开发应用程序,提供了一个开发服务器和实时重加载功能
html-webpack-plugin 简化了 HTML 文件的创建,以便为你的 webpack 包提供服务
babel-loader 用于让 webpack 知道如何运行 babel
@babel/core Babel 的核心包,用于转译 ES6+ 代码
@babel/preset-env Babel 预设,用于转译 ES6+ 代码
@babel/preset-react Babel 预设,用于转译 React 代码
style-loader 将 CSS 插入到 DOM 中
css-loader 解析 CSS 文件
postcss-loader 使用 PostCSS 处理 CSS 文件
@babel/plugin-syntax-class-properties 允许 Babel 解析 class 属性
mini-css-extract-plugin 从 JavaScript 文件中提取 CSS

2.2 webpack.config.js #

// 引入必要的模块
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const bootstrap = path.resolve('node_modules/bootstrap/dist/css/bootstrap.css');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// 导出一个函数,该函数接受两个参数:development 和 production
module.exports = ({ development, production }) => {
    const isEnvDevelopment = development === 'development';
    const isEnvProduction = production === 'production';
    // 定义一个函数,该函数接受一个参数 cssOptions,并返回一个 loader 数组
    const getStyleLoaders = (cssOptions) => {
        const loaders = [
            isEnvDevelopment && require.resolve('style-loader'),
            isEnvProduction && MiniCssExtractPlugin.loader,
            {
                loader: require.resolve('css-loader'),
                options: cssOptions,
            },
            'postcss-loader',
        ].filter(Boolean);
        return loaders;
    };
    // 返回一个 webpack 配置对象
    return {
        mode: isEnvProduction ? 'production' : isEnvDevelopment ? 'development' : 'development',
        devtool: isEnvProduction
            ? shouldUseSourceMap
                ? 'source-map'
                : false
            : isEnvDevelopment && 'cheap-module-source-map',
        cache: {
            type: 'filesystem'
        },
        entry: {
            main: './src/index.js'
        },
        optimization: {
            minimize: isEnvProduction,
            minimizer: [
                new TerserPlugin({ parallel: true })
            ],
            splitChunks: {
                chunks: 'all',
                minSize: 0,
                minRemainingSize: 0,
                maxSize: 0,
                minChunks: 1,
                maxAsyncRequests: 30,
                maxInitialRequests: 30,
                enforceSizeThreshold: 50000,
                cacheGroups: {
                    defaultVendors: {
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                        reuseExistingChunk: true
                    },
                    default: {
                        minChunks: 2,
                        priority: -20,
                        reuseExistingChunk: true
                    }
                }
            },
            runtimeChunk: {
                name: entrypoint => `runtime-${entrypoint.name}`,
            },
            moduleIds: isEnvProduction ? 'deterministic' : 'named',
            chunkIds: isEnvProduction ? 'deterministic' : 'named'
        },
        resolve: {
            modules: [path.resolve('node_modules')],
            extensions: ['.js'],
            alias: {
                bootstrap
            },
            fallback: {
                crypto: false,
                buffer: false,
                stream: false
            }
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: [
                        {
                            loader: 'babel-loader',
                            options: {
                                cacheDirectory: true,
                                presets: [
                                    "@babel/preset-react"
                                ]
                            }
                        }
                    ],
                    include: path.resolve('src'),
                    exclude: /node_modules/
                },
                {
                    test: /\.css$/,
                    use: getStyleLoaders({ importLoaders: 1 })
                }
            ]
        },
        devServer: {},
        plugins: [
            new HtmlWebpackPlugin(
                Object.assign(
                    {},
                    {
                        inject: true,
                        template: './public/index.html'
                    },
                    isEnvProduction
                        ? {
                            minify: {
                                removeComments: true,
                                collapseWhitespace: true,
                                removeRedundantAttributes: true,
                                useShortDoctype: true,
                                removeEmptyAttributes: true,
                                removeStyleLinkTypeAttributes: true,
                                keepClosingSlash: true,
                                minifyJS: true,
                                minifyCSS: true,
                                minifyURLs: true,
                            },
                        }
                        : undefined
                )
            )
        ]
    }
}

2.3 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')).render(
 "root"
);

2.4 public\index.html #

public\index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>react</title>
</head>
<body>

</body>
</html>

2.5 package.json #

package.json

{
   "scripts": {
       "build": "webpack --env=production",
       "dev": "webpack serve --env=development"
   },
}

2.6 mode #

mode 是 webpack 的一个重要配置选项,它用于设置 webpack 的运行模式。

在 webpack 5 中,mode 有三个可能的值:

  1. development: 这是用于开发环境的模式。当 mode 设置为 development 时,webpack 会优化构建速度和调试友好性,而不是生成的文件的大小。具体来说,这会启用以下一些 webpack 内置的插件和设置:

    • NamedChunksPlugin: 用于生成更易读的 chunk 名字。
    • NamedModulesPlugin: 用于生成更易读的模块名字。
    • 启用 Source Maps.
  2. production: 这是用于生产环境的模式。当 mode 设置为 production 时,webpack 会尽可能地优化构建的输出结果,包括压缩代码、删除未使用的代码、优化模块等。具体来说,这会启用以下一些 webpack 内置的插件和设置:

    • FlagDependencyUsagePlugin: 用于标记模块的依赖关系,以便 tree shaking 可以更好地移除未使用的代码。
    • FlagIncludedChunksPlugin: 用于标记包含其他模块的 chunk,以减少总体大小。
    • ModuleConcatenationPlugin: 用于优化模块,使浏览器能够更快地运行代码。
    • NoEmitOnErrorsPlugin: 用于在编译出错时跳过输出阶段,以确保输出资源不包含错误。
    • OccurrenceOrderPlugin: 用于对模块和 chunk 的 id 进行排序,以减小总体大小。
    • SideEffectsFlagPlugin: 用于标记模块是否有副作用,以便 tree shaking 可以更好地移除未使用的代码。
    • TerserPlugin: 用于压缩 JavaScript 代码。
  3. none: 不启用任何优化。这可以用于测试或者特殊的情况,通常不推荐在实际项目中使用。

通常,在开发时设置 modedevelopment,并在构建用于生产环境的版本时设置 modeproduction。这可以确保你在开发时得到最快的构建速度和最好的调试体验,并在生产环境中得到最小的文件大小和最高的性能。

2.7 devtool #

devtool 配置选项用于控制如何生成 source map。

在 webpack 5 中,devtool 的值可以是以下几种:

  1. false: 不生成 source map。
  2. eval: 每个模块用 eval 执行,且 source map 转为 DataUrl 添加到 eval 中。
  3. source-map: 生成完整的 source map,且生成单独的 source map 文件。
  4. inline-source-map: 生成完整的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。
  5. cheap-source-map: 生成没有列信息(column-mappings)的 source map,且生成单独的 source map 文件。
  6. cheap-module-source-map: 生成包含 loader 的 source maps 的 source map,且生成单独的 source map 文件。
  7. eval-source-map: 每个模块用 eval 执行,且生成完整的 source map。
  8. eval-cheap-source-map: 每个模块用 eval 执行,且生成没有列信息(column-mappings)的 source map。
  9. eval-cheap-module-source-map: 每个模块用 eval 执行,且生成包含 loader 的 source maps 的 source map。
  10. inline-cheap-source-map: 生成没有列信息(column-mappings)的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。
  11. inline-cheap-module-source-map: 生成包含 loader 的 source maps 的 source map,但是将 source map 转为 DataUrl 嵌入到 bundle 文件中。
  12. cheap-module-eval-source-map: 每个模块用 eval 执行,且生成包含 loader 的 source maps 的 source map。

以下是一些推荐的配置:

根据你的需要,你可以选择最适合你的 devtool 配置。例如,如果你不需要列信息,可以使用 cheap-source-map。如果你想将 source map 嵌入到 bundle 文件中,可以使用 inline-source-map

2.8 cache #

在 webpack 配置中,cache 选项用于配置缓存方式,以提高构建速度。

在你提供的代码中:

cache: {
    type: 'filesystem'
},

这段代码表示使用文件系统(filesystem)缓存。webpack 会将构建生成的中间文件缓存到文件系统中,这样在下次构建时,只需要重新构建更改了的部分,从而大大减少了构建时间。

webpack 提供了两种缓存方式:

  1. memory: 缓存在内存中。这是默认的缓存方式,但是一旦你关闭了 webpack 进程,缓存就会消失。

  2. filesystem: 缓存在文件系统中。这意味着即使你关闭了 webpack 进程,缓存也会保留,下次重新启动 webpack 时,会从缓存中读取数据。

因此,通过设置 cache.typefilesystem,你可以在多次构建之间保留缓存,从而提高构建速度。

2.9 optimization #

optimization 是 webpack 的一个配置项,用于控制优化过程的不同方面。这里是 webpack 5 中 optimization 的一些子属性的说明:

  1. minimize: boolean 类型。告诉 webpack 是否需要压缩输出的代码。默认情况下,在生产模式(production mode)中这个值为 true,而在开发模式(development mode)中这个值为 false

  2. minimizer: array 类型。这个数组定义了用于压缩代码的插件。默认情况下,webpack 使用 TerserPlugin 来压缩 JavaScript 代码。

  3. splitChunks: 这个对象控制了代码分割的行为。代码分割可以帮助你将代码分解成多个 bundle,这样可以更好地缓存代码,从而提高应用程序的加载速度。

    例如:

     splitChunks: {
         chunks: 'all',
         minSize: 0,
         minRemainingSize: 0,
         maxSize: 0,
         minChunks: 1,
         maxAsyncRequests: 30,
         maxInitialRequests: 30,
         enforceSizeThreshold: 50000,
         cacheGroups: {
             defaultVendors: {
                 test: /[\\/]node_modules[\\/]/,
                 priority: -10,
                 reuseExistingChunk: true
             },
             default: {
                 minChunks: 2,
                 priority: -20,
                 reuseExistingChunk: true
             }
         }
     },
    
    • chunks: 定义了哪些 chunk 会被优化。它可以是以下三个值之一: initial, async, all
    • minSize: 生成 chunk 的最小体积(以字节为单位)。默认值是 20000(20KB)。
    • minRemainingSize: 分割前和分割后的最小剩余大小,以确保剩余的部分没有比 minSize 更小的。
    • maxSize: 生成 chunk 的最大体积(以字节为单位)。
    • minChunks: 分割一个模块前,这个模块至少需要在多少个 chunk 中被引用。
    • maxAsyncRequests: 按需加载的 chunk 最大的并行请求数。
    • maxInitialRequests: 一个入口点的最大并行请求数。
    • enforceSizeThreshold: 忽略 maxSize,对于符合条件的 chunk,总是返回 truefalse
    • cacheGroups: 一个对象,它的键是缓存组的名称,值是一个对象,该对象定义了缓存组的行为。
  4. runtimeChunk: 这个选项允许你将运行时代码(runtime code)提取到一个单独的 chunk 中。这个 chunk 包含了所有的运行时代码,可以在生成多个 chunk 时,用于加载其他的 chunk。

  5. moduleIdschunkIds: 这两个选项用于控制 modulechunk 的 id 的生成。它们可以是以下四个值之一: natural, named, deterministic, size.

    • natural: 使用自然数作为 id。
    • named: 使用模块的路径作为 id。
    • deterministic: 生成一个短的、数字的 id。
    • size: 根据模块的大小生成 id。

2.10 resolve #

在 webpack 5 中,resolve 对象用于配置模块解析方式。

resolve 对象的属性包括:

  1. modules: 指定 webpack 应该查找模块的目录。默认值是 ["node_modules"],意味着 webpack 会在当前目录的 node_modules 目录中查找模块。
modules: [path.resolve('node_modules')]
  1. extensions: 指定在导入语句中没有指定扩展名时,webpack 应该尝试使用的扩展名列表。
extensions: ['.js']

这意味着当你导入模块时,如果你没有指定扩展名,webpack 会尝试将 .js 添加到模块名后面,然后查找该文件。

  1. alias: 用于设置模块的别名。
alias: {
    bootstrap
}

在这个示例中,bootstrap 被映射到了 node_modules/bootstrap/dist/css/bootstrap.css

  1. fallback: 允许你为 Node.js 全局变量和模块设置替代。这是 webpack 5 的新选项,用于替代 webpack 4 中的 node 选项。
fallback: {
    crypto: false,
    buffer: false,
    stream: false
}

在这个示例中,我们设置了 cryptobufferstream 的值为 false,这意味着 webpack 不会为这些模块提供任何替代。这是因为我们知道我们的代码不会在 Node.js 环境中运行,所以我们不需要这些模块。

以上就是 resolve 对象的一些常用属性,它们可以用来定制 webpack 的模块解析逻辑。

2.11 module #

在 webpack 5 的配置中,module 是一个对象,它用于定义如何处理项目中的不同类型的模块。

module 对象通常包含以下属性:

  1. rules (必须): 这是一个数组,包含一系列规则。每个规则可以包含以下条件属性:

    • test: 一个正则表达式,用于测试文件是否应该被 loader 处理。例如,/\.js$/ 会匹配所有 .js 文件。
    • include: 一个条件,指示应该检查哪些文件。
    • exclude: 一个条件,指示应该排除哪些文件。
    • use: 指定应该对匹配文件使用哪些 loader。这可以是一个 loader 字符串,也可以是一个对象,包含 loaderoptions 属性。
    • loaderoptions: 和 use 类似,但只能用于一个 loader。

      示例:

      module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
      }
      

      这个例子中的 rules 配置表示:对所有 .js 文件(除了 node_modules 目录下的文件)使用 babel-loader,并为 babel-loader 提供 @babel/preset-env 预设。

  2. noParse: 一个正则表达式,或者一个正则表达式数组,用于排除一些文件,使这些文件不被模块解析器解析。

2.12 devServer #

在 webpack 5 配置中,devServer 是一个对象,用于配置 webpack-dev-server 的行为。

webpack-dev-server 是一个小型的 Node.js Express 服务器,用于提供通过 webpack 构建的资源、进行热模块更新和页面重载等功能。

devServer 对象可以包含以下属性:

  1. contentBase: 类型:string | array。告诉服务器从哪个目录中提供内容。默认情况下,将使用当前工作目录作为提供内容的目录,但是你可以修改为其他目录。

  2. compress: 类型:boolean。是否启用 gzip 压缩。

  3. port: 类型:number。指定要监听请求的端口号。

  4. hot: 类型:boolean。是否启用模块热替换功能。

  5. open: 类型:boolean。是否自动打开浏览器。

  6. historyApiFallback: 类型:boolean | object。当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html

  7. proxy: 类型:object。用于将某些 API 请求代理到另一个服务器。

  8. publicPath: 类型:string。此路径下的打包文件可在浏览器中访问。

  9. overlay: 类型:boolean | object。当存在编译器错误或警告时,在浏览器中显示全屏覆盖。

示例:

devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000,
    open: true,
    hot: true,
    historyApiFallback: true,
    proxy: {
        '/api': 'http://localhost:3000'
    },
    publicPath: '/',
    overlay: true
}

2.13 html-webpack-plugin #

html-webpack-plugin 是一个 webpack 的插件,用于简化 HTML 文件的创建,该文件将包含 webpack 打包生成的所有 bundle。

该插件会在输出目录中生成一个 HTML 文件,并在该文件中引入 webpack 打包生成的所有 JavaScript 和 CSS 文件。

你可以通过配置 html-webpack-plugin 来控制生成的 HTML 文件的内容。

在你的 webpack 配置文件中,你需要先 require html-webpack-plugin,然后在 plugins 数组中创建 html-webpack-plugin 的一个实例。

例如:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ... 其他配置
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html'
        })
    ]
}

在这个例子中,template 选项表示使用 ./src/index.html 文件作为模板来生成最终的 HTML 文件。

html-webpack-plugin 插件有很多可配置的选项:

  1. title: 类型:string。生成的 HTML 文件的标题。

  2. filename: 类型:string。输出的 HTML 文件的文件名。

  3. template: 类型:string。模板文件的路径。

  4. inject: 类型:boolean | string。注入选项。有四个选项值:truefalse'head''body'true'body' 表示所有 JavaScript 资源将被放置在 <body> 元素的底部。'head' 表示将 JavaScript 资源放置在 <head> 元素中。false 不会注入 js 文件。

  5. favicon: 类型:string。favicon 路径。

  6. meta: 类型:object。添加到 HTML <head> 中的 meta 标签。

  7. base: 类型:string | object。添加到 HTML <head> 中的 base 标签。

  8. minify: 类型:boolean | object。控制是否最小化输出的 HTML。

  9. hash: 类型:boolean。如果为 true,则将唯一的 webpack 编译哈希值附加到所有包含的脚本和 CSS 文件。这对于清除缓存很有用。

  10. showErrors: 类型:boolean。是否将错误详细信息写入 HTML 页面。

  11. chunks: 类型:array。指定要包含的块。

  12. excludeChunks: 类型:array。指定要排除的块。

3.路由切换优化 #

3.1 src\index.js #

import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
const Home = lazy(() => import('./components/Home'));
const User = lazy(() => import('./components/User'));
const Loading = () => <div>Loading</div>;
ReactDOM.createRoot(document.getElementById('root')).render(
    <Router>
        <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/user">User</Link></li>
        </ul>
        <Suspense fallback={<Loading />}>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/user" element={<User />} />
            </Routes>
        </Suspense>
    </Router>
);

3.2 User.js #

src\components\User.js

import React from 'react';
function Home(){
    return <div>Home</div>
}
export default Home;

3.3 Home.js #

src\components\Home.js

import React from 'react';
function Home(){
    return <div>Home</div>
}
export default Home;

4.更新阶段优化 #

4.1 PureComponent #

4.1.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import PureComponent from './PureComponent';
ReactDOM.createRoot(document.getElementById('root')).render(
    <PureComponent/>
);

4.1.2 src\PureComponent.js #

src\PureComponent.js

import React from 'react';
import { PureComponent, memo } from './utils';
export default class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = { title: '计数器', number: 0 }
    }
    add = (amount) => {
        this.setState({ number: this.state.number + amount });
    }
    render() {
        console.log('App render');
        return (
            <div>
                <Counter number={this.state.number} />
                <button onClick={() => this.add(1)}>+1</button>
                <button onClick={() => this.add(0)}>+0</button>
                <ClassTitle title={this.state.title} />
                <FunctionTitle title={this.state.title} />
            </div>
        )
    }
}
class Counter extends PureComponent {
    render() {
        console.log('Counter render');
        return (
            <p>{this.props.number}</p>
        )
    }
}
class ClassTitle extends PureComponent {
    render() {
        console.log('ClassTitle render');
        return (
            <p>{this.props.title}</p>
        )
    }
}
const FunctionTitle = memo(props => {
    console.log('FunctionTitle render');
    return <p>{props.title}</p>;
});

4.1.3 src\utils.js #

src\utils.js

import React from 'react';
export class PureComponent extends React.Component{
    shouldComponentUpdate(nextProps,nextState){
        return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
    }
}
export function memo(OldComponent){
    return class extends PureComponent{
      render(){
        return <OldComponent {...this.props}/>
      }
}
}
export function shallowEqual(obj1,obj2){
    if(obj1 === obj2)
        return true;
    if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
        return false;
    }    
    let keys1 = Object.keys(obj1);
    let keys2 = Object.keys(obj2);
    if(keys1.length !== keys2.length){
        return false;
    }
    for(let key of keys1){
        if(!obj2.hasOwnProperty(key) || obj1[key]!== obj2[key]){
            return false;
        }
    }
    return true;
}

4.2 immutable #

Immutable.js 是一个 JavaScript 库,它提供了一系列的不可变数据结构。在 JavaScript 中,对象和数组是可变的,这意味着你可以改变对象的属性或数组的元素。但是,这有时会导致不可预见的副作用,特别是在大型应用程序或者多人合作的项目中。

不可变数据结构是一旦创建,就不能再被更改的数据结构。如果你想改变一个不可变对象,你必须创建一个新的对象。

这有几个好处:

  1. 可预测性: 由于数据是不可变的,你可以更容易地预测程序的行为。
  2. 性能: Immutable.js 使用结构共享,这意味着如果对象在修改后保持不变,那么它将指向原始对象,从而节省内存。
  3. 易于调试: 不可变数据可以帮助你更轻松地跟踪数据的更改。
  4. 简化复杂性: 由于你不需要担心数据被意外更改,因此可以简化代码。

Immutable.js 提供了几个不可变数据结构,例如 ListMapSetOrderedMapStack 等。这些数据结构类似于 JavaScript 的原生数组和对象,但它们是不可变的。

下面是一些基本示例:

const { Map, List } = require('immutable');

// 创建一个不可变的 map
const map1 = Map({ a: 1, b: 2, c: 3 });

// 创建一个新的 map,而不是修改原始 map
const map2 = map1.set('b', 50);
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50

// 创建一个不可变的 list
const list1 = List([1, 2, 3]);

// 创建一个新的 list,而不是修改原始 list
const list2 = list1.push(4);
console.log(list1.size); // 3
console.log(list2.size); // 4

在这个例子中,map1list1 是不可变的,因此当我们想要添加一个新的元素或更改一个现有的元素时,我们不会更改原始数据结构,而是创建一个新的数据结构。

注意,由于 Immutable.js 数据结构是不可变的,所以它们的方法不会更改原始数据结构,而是返回一个新的数据结构。这是一个重要的区别,与 JavaScript 的原生数组和对象的方法不同,这些方法通常会更改原始数据结构。

4.2.1 PureComponent.js #

src\PureComponent.js

import React from 'react';
import { PureComponent, memo } from './utils';
+import { Map } from "immutable";
export default class App extends React.Component {
    constructor(props) {
        super(props);
+       this.state = {count:Map({ number: 0 })}
    }
    add = (amount) => {
+       let count = this.state.count.set('number',this.state.count.get('number') + amount);
+       this.setState({count});
    }
    render() {
        console.log('App render');
        return (
            <div>
+               <Counter number={this.state.count.get('number')}/>
                <button onClick={() => this.add(1)}>+1</button>
                <button onClick={() => this.add(0)}>+0</button>
                <ClassTitle title={this.state.title} />
                <FunctionTitle title={this.state.title} />
            </div>
        )
    }
}
class Counter extends PureComponent {
    render() {
        console.log('Counter render');
        return (
            <p>{this.props.number}</p>
        )
    }
}
class ClassTitle extends PureComponent {
    render() {
        console.log('ClassTitle render');
        return (
            <p>{this.props.title}</p>
        )
    }
}
const FunctionTitle = memo(props => {
    console.log('FunctionTitle render');
    return <p>{props.title}</p>;
});

4.2.2 utils.js #

src\utils.js

import React from 'react';
+import { is } from "immutable";
export class PureComponent extends React.Component{
    shouldComponentUpdate(nextProps,nextState){
        return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
    }
}
export function memo(OldComponent){
    return class extends PureComponent{
      render(){
        return <OldComponent {...this.props}/>
      }
}
}
export function shallowEqual(obj1,obj2){
    if(obj1 === obj2)
        return true;
    if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
        return false;
    }    
    let keys1 = Object.keys(obj1);
    let keys2 = Object.keys(obj2);
    if(keys1.length !== keys2.length){
        return false;
    }
    for(let key of keys1){
+       if (!obj2.hasOwnProperty(key) || !is(obj1[key],obj2[key])) {
            return false;
        }
    }
    return true;
}

4.3 reselect #

reselect 是一个简单的 selector 库,用于构建可记忆的、可组合的 selector 函数。reselect 通常与 Redux 一起使用,但它不是 Redux 专用的,可以在任何应用程序中使用。

Redux 应用程序中,selector 函数用于从 Redux 存储中检索特定的数据片段。selector 是一个接受整个 Redux 存储树并返回所需数据的函数。

reselect 提供了一种创建可记忆的 selector 的方法。reselectcreateSelector 函数会记住参数,只有当参数发生变化时才重新计算结果。这可以提高性能,因为如果参数没有变化,selector 将直接返回上一次的计算结果,而不是重新计算。

下面是一个简单的例子:

// 引入 reselect 库
import { createSelector } from 'reselect';
// 引入 redux 库
import { legacy_createStore as createStore, combineReducers } from 'redux';
// 初始化待办事项的id
let nextTodoId = 0;
// 定义添加待办事项的action
const addTodo = text => ({
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
});
// 定义设置可见性过滤器的action
const setVisibilityFilter = filter => ({
    type: 'SET_VISIBILITY_FILTER',
    filter
});
// 定义可见性过滤器的常量
const VisibilityFilters = {
    SHOW_ALL: 'SHOW_ALL',
    SHOW_COMPLETED: 'SHOW_COMPLETED',
    SHOW_ACTIVE: 'SHOW_ACTIVE'
};
// 解构可见性过滤器的常量
const {
    SHOW_ALL
} = VisibilityFilters;
// 定义可见性过滤器的reducer
function visibilityFilter(state = SHOW_ALL, action) {
    switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
            return action.filter;
        default:
            return state;
    }
}
// 定义待办事项的reducer
function todos(state = [], action) {
    switch (action.type) {
        case 'ADD_TODO':
            return [...state, {
                text: action.text,
                completed: false
            }];
        default:
            return state;
    }
}
// 使用 combineReducers 合并 reducers
const todoApp = combineReducers({
    visibilityFilter,
    todos
});
// 创建 Redux store
const store = createStore(todoApp);
// 打印初始状态
console.log(store.getState());
// 订阅状态变化
const unsubscribe = store.subscribe(() => console.log(store.getState()));
// 分发一些 actions
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));
// 停止订阅状态变化
unsubscribe();
// 定义输入选择器
const getVisibilityFilter = state => state.visibilityFilter;
const getTodos = state => state.todos;
// 定义一个可记忆的 selector
const getVisibleTodos = createSelector(
    [getVisibilityFilter, getTodos],
    (visibilityFilter, todos) => {
        switch (visibilityFilter) {
            case 'SHOW_ALL':
                return todos;
            case 'SHOW_COMPLETED':
                return todos.filter(t => t.completed);
            case 'SHOW_ACTIVE':
                return todos.filter(t => !t.completed);
            default:
                throw new Error('Unknown filter: ' + visibilityFilter);
        }
    }
);
// 定义 mapStateToProps 函数
const mapStateToProps = state => {
    return {
        todos: getVisibleTodos(state)
    };
};
// 打印 mapStateToProps 函数的返回值
console.log(mapStateToProps(store.getState()));

在这个例子中,getVisibilityFiltergetTodos 是输入选择器。它们都是简单的函数,接受整个 Redux 存储树并返回所需的数据。

getVisibleTodos 是我们使用 reselectcreateSelector 函数创建的可记忆 selector。这个 selector 接受 getVisibilityFiltergetTodos 作为输入选择器,并返回一个函数,该函数计算并返回我们需要的数据。

mapStateToProps 函数使用 getVisibleTodos selectorRedux 存储中检索可见的待办事项。每次存储更改时,mapStateToProps 函数都会被调用,但是只有当 getVisibilityFiltergetTodos 返回的值发生变化时,getVisibleTodos selector 才会重新计算结果。

这样可以提高性能,因为如果 visibilityFiltertodos 没有变化,getVisibleTodos selector 将直接返回上一次的计算结果,而不是重新计算。

5.大数据渲染 #

5.1 时间分片 #

5.1.1 优化前 #

src\Home.js

import React from 'react';
export default class Home extends React.Component {
    state = {
        list: []
    }
    handleClick = () => {
        let starTime = new Date().getTime();
        this.setState({
            list: new Array(30000).fill(0)
        }, () => {
            const end = new Date().getTime()
            console.log((end - starTime) / 1000 + '秒')
        })
    }
    render() {
        return (
            <ul>
                <button onClick={this.handleClick}>点击</button>
                {
                    this.state.list.map((item, index) => (
                        <li key={index} >{index}</li>
                    ))
                }
            </ul>
        )
    }
}

5.1.2 优化后 #

src\Home.js

import React from 'react';
export default class Home extends React.Component {
    state = {
        list: []
    }
+    handleClick = () => {
+        this.timeSlice(550);
+    }
+    timeSlice = (times) => {
+        //requestIdleCallback
+        requestAnimationFrame(() => {
+            let minus = times >= 100 ? 100 : times;
+            times -= minus;
+            this.setState({
+                list: [...this.state.list, ...new Array(minus).fill(0)]
+            }, () => {
+                if (times > 0) {
+                    this.timeSlice(times);
+                }
+            });
+        });
+    }
    render() {
        return (
            <ul>
                <button onClick={this.handleClick}>点击</button>
                {
                    this.state.list.map((item, index) => (
                        <li key={index} >{index + 1}</li>
                    ))
                }
            </ul>
        )
    }
}

5.1.3 requestAnimationFrame #

requestAnimationFrame是一个浏览器提供的API,用于在下一次重绘之前请求浏览器调用指定的函数更新动画。这个API是非常重要的,因为它允许你创建更流畅、更高效的动画。

在浏览器中,屏幕通常每秒刷新60次,也就是每16.7毫秒刷新一次。requestAnimationFrame允许你在每次重绘之前执行一些代码,这样你可以更精确地控制动画的帧率。

使用方法

使用requestAnimationFrame通常包含以下步骤:

  1. 定义一个函数:这个函数将在下一次重绘之前被浏览器调用。
  2. 注册这个函数:使用window.requestAnimationFrame()将这个函数注册到浏览器。
  3. 在函数中更新动画:你可以在这个函数中更新动画的状态。
  4. 再次注册:如果你的动画还没有完成,可以在函数的末尾再次注册这个函数。

示例

下面是一个使用requestAnimationFrame的简单示例:

// 定义一个函数,这个函数将在下一次重绘之前被浏览器调用
function updateAnimation(timestamp) {
    // 更新动画的状态
    // ...

    // 如果动画还没有完成,再次注册这个函数
    requestAnimationFrame(updateAnimation);
}

// 注册这个函数
requestAnimationFrame(updateAnimation);

在这个示例中,updateAnimation函数将在每次重绘之前被浏览器调用,你可以在这个函数中更新动画的状态。如果动画还没有完成,可以在函数的末尾再次注册updateAnimation函数。

注意事项

  1. 浏览器兼容性requestAnimationFrame被所有现代浏览器支持,但在一些旧的浏览器中可能需要使用前缀,比如webkitRequestAnimationFrame
  2. 性能requestAnimationFramesetTimeoutsetInterval更高效,因为它是专门为动画设计的。它会尽可能地与浏览器的刷新频率保持同步,从而获得最流畅的动画效果。
  3. 暂停:如果页面不可见或者最小化,requestAnimationFrame回调将不会被触发,直到页面变得可见时才会继续。
  4. 时间戳requestAnimationFrame会给你的函数传递一个时间戳参数,这个时间戳表示从页面加载到当前的时间。

总的来说,requestAnimationFrame是一个非常重要和强大的API,它允许你创建流畅、高效、低功耗的动画。

5.1.4 requestIdleCallback #

requestIdleCallback是一个浏览器提供的API,允许你在浏览器的空闲时段内执行一些低优先级的任务。这个API是非常有用的,因为它允许你不影响帧率、用户交互等高优先级任务的情况下执行一些后台任务。

浏览器的主线程通常非常繁忙,需要处理用户事件、渲染、布局、JavaScript执行等等。如果你在主线程中执行一些耗时的任务,可能会导致页面卡顿或不响应。requestIdleCallback可以帮助你更智能地安排这些任务。

使用方法

使用requestIdleCallback通常包含以下步骤:

  1. 定义一个函数:这个函数将在浏览器空闲时被调用。
  2. 注册这个函数:使用window.requestIdleCallback()将这个函数注册到浏览器。
  3. 在函数中检查空闲时间:浏览器会传递一个IdleDeadline对象给你的函数,你可以使用这个对象的timeRemaining方法来获取浏览器还有多少空闲时间。
  4. 取消注册:如果你的函数没有在一个空闲周期内完成,你可以使用cancelIdleCallback取消它。

示例

下面是一个使用requestIdleCallback的简单示例:

// 定义一个函数,这个函数将在浏览器空闲时被调用
function doSomeHeavyWork(deadline) {
    while (deadline.timeRemaining() > 0) {
        // 执行一些耗时的任务
    }
}

// 注册这个函数
var id = requestIdleCallback(doSomeHeavyWork);

// 如果你想取消这个函数,可以这样做
cancelIdleCallback(id);

注意事项

  1. 浏览器兼容性:不是所有的浏览器都支持requestIdleCallback。在使用之前,你需要检查浏览器的兼容性或使用polyfill。
  2. 不是所有任务都适合requestIdleCallback适合一些不需要立即执行、可以在空闲时执行的任务,比如统计、日志、本地存储等。
  3. 时间限制:浏览器会给你的函数分配一个时间片,这个时间片通常是50ms。但是,如果浏览器很繁忙,这个时间片可能会更短。
  4. 不适合动画:如果你需要执行动画,不应该使用requestIdleCallback。对于动画,应该使用requestAnimationFrame

总的来说,requestIdleCallback是一个非常有用的API,可以帮助你更智能地安排一些后台任务,提升页面的性能和响应速度。

5.2 虚拟列表 #

在React中,虚拟列表(Virtual List)或窗口化列表(Windowing)是一种优化技术,用于提高大列表或表格的渲染性能。

问题背景

在React中,如果你需要渲染一个包含大量元素的列表,通常会直接将所有的元素一次性渲染到DOM中。然而,这样做可能会导致一些性能问题:

  1. 加载时间变长:渲染大量的元素会消耗大量的CPU和内存,导致页面的加载时间变长。
  2. 滚动卡顿:浏览器需要维护一个大的DOM树,这可能会导致滚动时出现卡顿。
  3. 内存占用高:即使有些元素不可见,浏览器仍然需要为它们分配内存。

解决方案:虚拟列表

虚拟列表是一种解决这些问题的优化技术。它的基本思想是:只渲染当前可见的元素,而不是一次性渲染所有的元素。

虚拟列表通常包括以下几个步骤:

  1. 计算可见元素:根据列表的当前滚动位置,计算出当前可见的元素。
  2. 只渲染可见元素:只将当前可见的元素渲染到DOM中。
  3. 占位:为不可见的元素放置一个占位元素,以保持列表的总高度不变。

通过这种方式,虚拟列表可以极大地提高列表的渲染性能,减少内存的占用,并提高滚动的流畅度。

实现

虚拟列表可以手动实现,但通常推荐使用一些成熟的库,比如react-windowreact-virtualized

这里是一个使用react-window的简单示例:

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const VirtualList = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

export default VirtualList;

在这个示例中,我们首先导入react-window库的FixedSizeList组件。然后,我们创建一个Row组件,它接收indexstyle参数,并渲染一个列表项。

接下来,我们创建VirtualList组件,它使用FixedSizeList组件并传递一些参数:

然后,我们将Row组件作为子组件传递给FixedSizeList

通过这种方式,react-window会自动计算当前可见的元素,并只渲染这些元素。

总结

虚拟列表是一种非常重要的优化技术,它可以极大地提高大列表的渲染性能,减少内存的占用,并提高滚动的流畅度。react-windowreact-virtualized是两个非常成熟的库,可以帮助你更容易地实现虚拟列表。

5.2.1 src\index.js #

import React from 'react';
import ReactDOM from 'react-dom/client';
import VirtualList from './components/VirtualList';
const data = new Array(30).fill(0);
ReactDOM.createRoot(document.getElementById('root')).render(
    <VirtualList
    width='50%'
    height={500}
    itemCount={data.length}
    itemSize={50}
    renderItem={(data) => {
        let { index, item, style } = data;
        console.log(data);
        return (
            <div key={index} style={{ ...style, backgroundColor: index % 2 === 0 ? 'green' : 'orange' }}>
                {index+1}
            </div>
        )
    }
    }
/>
);

5.2.2 VirtualList.js #

src\components\VirtualList.js

import React from 'react';
export default class Index extends React.Component {
    scrollBox = React.createRef()
    state = { start: 0 }
    handleScroll = () => {
        const { itemSize } = this.props;
        const { scrollTop } = this.scrollBox.current;
        const start = Math.floor(scrollTop / itemSize);
        this.setState({ start })
    }
    render() {
        const { height, width, itemCount, itemSize, renderItem } = this.props;
        const { start } = this.state;
        let end = start + Math.floor(height / itemSize) + 1;
        end = end > itemCount ? itemCount : end;
        const visibleList = new Array(end - start).fill(0).map((item, index) => ({ index: start + index }));
        const style = { position: 'absolute', top: 0, left: 0, width: '100%', height: itemSize };
        return (
            <div
                style={{ overflow: 'auto', willChange: 'transform', height, width }}
                ref={this.scrollBox}
                onScroll={this.handleScroll}
            >
                <div style={{ position: 'absolute', width: '100%', height: `${itemCount * itemSize}px` }}>
                    {
                        visibleList.map(({ index }) => renderItem({ index, style: { ...style, top: itemSize * index } }))
                    }
                </div>
            </div>
        )
    }
}