React 性能优化指南

  • useState: 传入函数, 避免重复计算。
  • useMemo: 缓存计算结果,缓存数据。
  • useCallback: 缓存函数。
  • React.memo: 缓存子组件。
  • 优化代码体积,使用source-map-explorer分析代码体积,使用webpack-bundle-analyzer分析代码结构。

useState

  • useState 参数传入函数时,渲染只会调用一次(初始化时候),后续更新不会再调用。
  • 适合场景:当初始化的值需要复杂计算时,可以使用函数传入,避免重复计算。
import React, { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(() => {
    // 仅在初始化时调用,结果会被缓存
    console.log('render')
    return 0
  })

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

useMemo

  • useMemo: 可以缓存数据,不用每次执行函数都重新生成。
  • 适合场景: 可用用户计算量比较大的场景,缓存提高性能。
  • 使用 useMemo,要注意依赖项,当依赖项发生变化时,会重新计算, 如果依赖项经常变化,缓存的成本要较高,所以要去权衡。
import React, { useState, useMemo } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  const [value, setValue] = useState('')

  // 缓存数据
  const expensive = useMemo(() => {
    console.log('compute')
    let sum = 0
    for (let i = 0; i < count * 100; i++) {
      sum += i
    }
    return sum
  }, [count])

  return (
    <div>
      <h4>
        {count}-{expensive}
      </h4>
      <button onClick={() => setCount(count + 1)}>+</button>
      <input value={value} onChange={(event) => setValue(event.target.value)} />
    </div>
  )
}

useCallback

  • useCallback: 缓存函数,不用每次渲染都重新生成函数。
  • 适用场景:当子组件接收函数作为 props 时,可以使用 useCallback 缓存函数,避免子组件重复渲染, 也可以在组件中,经常调用函数进行缓存 。
import React, { useState, useCallback } from 'react'

export default function App() {
  const [count, setCount] = useState(0)
  const [value, setValue] = useState('')

  // 缓存函数
  const callback = useCallback(() => {
    console.log('callback')
    return count
  }, [count])

  return (
    <div>
      <h4>
        {count}-{callback()}
      </h4>
      <button onClick={() => setCount(count + 1)}>+</button>
      <input value={value} onChange={(event) => setValue(event.target.value)} />
    </div>
  )
}

React.memo

  • React.memo: 缓存子组件,避免重复渲染。
  • 适用场景:当子组件只依赖 props 时,可以使用 React.memo 缓存子组件,避免重复渲染。
import React, { useState, memo } from 'react'

// 缓存子组件
const Child = memo(function Child({ data }) {
  console.log('render child')
  return <div>{data}</div>
})

// 父组件
export default function App() {
  const [count, setCount] = useState(0)
  const [value, setValue] = useState('')

  return (
    <div>
      <h4>{count}</h4>
      <button onClick={() => setCount(count + 1)}>+</button>
      <input value={value} onChange={(event) => setValue(event.target.value)} />
      <Child data={value} />
    </div>
  )
}

优化代码体积

  • 使用source-map-explorer分析代码体积,使用webpack-bundle-analyzer分析代码结构。

  • 安装source-map-explorer

npm install source-map-explorer  --save-dev
  • package.json 中添加命令。
{
  "scripts": {
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  }
}
  • 运行命令。
npm run build
npm run analyze
  • 在路由使用懒加载

    import React, { lazy, Suspense } from 'react'
    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
    
    const Home = lazy(() =>
      import(
        /* webpackChunkName: "home" */
        './pages/Home'
      )
    )
    const About = lazy(() =>
      import(
        /* webpackChunkName: "about" */
        './pages/About'
      )
    )
    
    export default function App() {
      return (
        <Router>
          <Suspense fallback={<div>Loading...</div>}>
            <Switch>
              <Route path='/' exact component={Home} />
              <Route path='/about' component={About} />
            </Switch>
          </Suspense>
        </Router>
      )
    }
    
    • 抽离公共代码

    • craco.config.js 中配置 splitChunks

    • terser-webpack-plugin 开启压缩。

    • compression-webpack-plugin 开启 gzip 压缩。

    • tree shakingwebpack 默认支持 tree shaking,但是需要注意的是,tree shaking 只支持 ES Module 的引入方式,不支持 CommonJS 的引入方式。

      • ES Module 的引入方式:import { Button } from 'antd'
      • 配置 optimization.usedExportstrue 开启 tree shaking
    const path = require('path')
    const { name } = require('./package.json')
    const TerserPlugin = require('terser-webpack-plugin')
    
    const pathResolve = (pathUrl) => path.join(__dirname, pathUrl)
    
    module.exports = {
      webpack: {
        configure(webpackConfig) {
          // 配置扩展扩展名优化
          webpackConfig.resolve.extensions = [
            '.tsx',
            '.ts',
            '.jsx',
            '.js',
            '.scss',
            '.css',
            '.json',
          ]
          // 抽离公共代码
          if (process.env.NODE_ENV === 'production') {
            // splitChunks打包优化
            webpackConfig.optimization.splitChunks = {
              chunks: 'all',
              // 策略配置
              cacheGroups: {
                antd: {
                  name: 'chunk-antd', // 单独将 antd 拆包
                  test: /antd/,
                  priority: 100,
                },
                reactDom: {
                  name: 'chunk-react-dom', // 单独将 react-dom 拆包
                  test: /react-dom/,
                  priority: 99,
                },
                vendors: {
                  test: /[\\/]node_modules[\\/]/,
                  name: 'chunk-vendors',
                  priority: 98,
                },
              },
            }
    
            // 插件配置
            webpackConfig.plugins = webpackConfig.plugins.concat([
              // 开启gzip压缩
              new CompressionWebpackPlugin({
                test: /\.(js|ts|jsx|tsx|css|scss)$/, //匹配要压缩的文件
                algorithm: 'gzip', // gzip or brotli plugin
              }),
            ])
    
            // tree shaking 开启
            // webpackConfig.optimization.usedExports = true
            // js 压缩
            webpackConfig.optimization.minimizer = [
              new TerserPlugin({
                terserOptions: {
                  parallel: true, // 开启多进程压缩
                  compress: {
                    drop_console: true,
                    drop_debugger: true,
                  },
                },
              }),
            ]
          } else {
            // 默认配置
            webpackConfig.optimization.splitChunks = {}
          }
    
          // 开启持久化缓存
          webpackConfig.cache.type = 'filesystem'
    
          return webpackConfig
        },
      },
    }
    

    参考资料

  • React 组件性能优化:如何避免不必要的 re-renderopen in new window

  • 基于 craco 配置的 react 项目的 webpack 构建优化open in new window

  • 浅谈一下在阿里工作的前端性能优化的全链路经验open in new window