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 shaking:- webpack默认支持- tree shaking,但是需要注意的是,- tree shaking只支持- ES Module的引入方式,不支持- CommonJS的引入方式。- ES Module的引入方式:- import { Button } from 'antd'
- 配置 optimization.usedExports为true开启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 }, }, }- 参考资料
