React 组件性能优化最佳实践

React 组件性能优化的核心是减少渲染真实 DOM 节点的频率, 减少 Virtual DOM 比对的频率

1、组件卸载前进行清理操作

在组件中为 window 注册的全局事件, 以及定时器 在组件卸载前要清理掉, 防止组件卸载后继续执行影响应用性能
需求: 开启定时器然后卸载组件,查看组件中的定时器是否还在运行。

2、PureComponent

1)什么是纯组件

纯组件会对组件输入数据进行浅层比较,如果当前输入数据和上次输入数据相同,组件不会重新渲染

2)什么是浅层比较

比较引用数据类型在内存中的引用地址是否相同,比较基本数据类型的值是否相同

3)如何实现纯组件

类组件继承 PureComponent 类,函数组件使用 memo 方法

4) 为什么不直接进行 diff 操作,而是要先进行浅层比较,浅层比较难道没有性能消耗吗

和进行 diff 比较的操作相比,浅层比较 将消耗更少的性能。 diff 操作会重新遍历整颗 virtual DOM 树,而浅层比较只操作当前组件的state 和 props
5)需求:在状态对象中存储 name 值为张三,组件挂载完成后将 name 属性的值再次更改为张三, 然后分别将 name 传递给纯组件和非纯组件,查看结果

3、shouldComponentUpdate

纯组件只能进行浅层比较,要进行深层笔比较,使用 shouldComponentUpate, 他用于编写自定义比较逻辑
返回 true 重新渲染组件,返回false 组织重新渲染
函数的第一个参数为 nextProps, 第二个参数为 nextState
需求: 在页面中展示员工信息, 员工信息包括,姓名、年龄、职位。 但是在页面中只想展示姓名和年龄,也就是说只有姓名和年龄发生变化时才有必要重新渲染组件,如果员工的其他信息发生了变化没必要重新渲染组件

4、React.memo

1) memo 使用

将函数组件变为纯组件,将当前 props 和 上一次的 props 进行浅层比较,如果相同就阻止组件重新渲染
需求: 父组件维护两个状态, index 和 name , 开启定时器让 index 不断发生变化, name 传递给子组件,查看父组件更新子组件是否也更新了

2)为memo 传递比较逻辑

memo 底层依然是浅层比较,要实现深层比较,要使用自定义比较逻辑
使用 memo 方法自定义比较逻辑,用于执行深层比较
比较函数的第一个参数为上一次props,比较函数的第二个参数为下一次的 props , 比较函数返回 true , 不进行渲染, 比较函数返回false, 组件重新渲染

  1. const ShowPersonMemo = memo(ShowPerson. comparePerson)
  2. function comparePerson(prevProps, nextProps) {
  3. if (
  4. prevProps.perspon.name !== nextProps.person.name ||
  5. prevProps.perspon.age !== nextProps.person.age
  6. ) {
  7. // 修改的是 name 和age 重新渲染
  8. return false
  9. }
  10. // 否则不进行渲染
  11. return true
  12. }

5、使用组建懒加载

使用组件懒加载可以减少 bundle 文件大小, 加快组建呈递速度

1)路由组件懒加载

lazy 和 Suspense 需结合使用

  1. import React, { lazy, Suspense } from "react"
  2. import { BrowserRouter, Link, Route, Switch } from "react-router-dom"
  3. const Home = lazy( () => import("./Home"))
  4. const Home = lazy( () => import("./List"))
  5. function App() {
  6. return (
  7. <BrowserRouter>
  8. <Link to="/">Home</Link>
  9. <Link to="/list">List</Link>
  10. <Switch>
  11. <Suspense fallback={<div>loading...</div>}>
  12. <Route path="/" component={Home} exact />
  13. <Route path="/list" component={List} />
  14. </Suspense>
  15. </Switch>
  16. </BrowserRouter>
  17. )
  18. }
  19. export default App

2)根据条件进行组件懒加载

适用于组件不会随条件频繁切换

  1. import React, { lazy, Suspense } from "react"
  2. function App() {
  3. let LazyComponent = null
  4. if (true) {
  5. LazyComponent = lazy( () => import("./Home"))
  6. } else {
  7. // 通过 webpackChunkName 来修改打包时 组件的名称
  8. LazyComponent = lazy( () => import(/* webpackChunkName: "list"*/"./List"))
  9. }
  10. return (
  11. <Suspense fallback={<div>loading...</div>}>
  12. <LazyComponent />
  13. </Suspense>
  14. )
  15. }

6、使用Fragment 避免额外标记

React 组件中返回的 jsx 如果有多个同级元素, 多个同级元素必须要有一个共同的父级

  1. function App() {
  2. return (
  3. <div>
  4. <div>message</div>
  5. <div>message</div>
  6. </div>
  7. )
  8. }

为了满足这个条件我们通常都会在最外层添加一个div, 但是这样的话就会多出一个无意义的标记, 如果每个组件都多出这样一个无意义的标记的话,浏览器渲染引擎的负担就会加剧
为了解决这个问题, React 推出了 frament 占位符标记, 使用占位符标记 及满足了拥有共同父级的要求又不会多出额外的无意义标记

  1. import { Fragment } from "react"
  2. function App() {
  3. return (
  4. <Fragment>
  5. <div>message</div>
  6. <div>message</div>
  7. </Fragment>
  8. )
  9. }
  10. 或者:
  11. function App() {
  12. return (
  13. <>
  14. <div>message</div>
  15. <div>message</div>
  16. </>
  17. )
  18. }

7、不要使用内联函数定义

在使用内联函数后, render 方法每次运行时都会创建该函数的新实例,导致 React 在进行 Virtual DOM 比对时, 新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例, 而旧的函数实例又要交给垃圾回收器处理

8、在构造函数中进行函数this绑定

在类组件中如果使用 fn(){} 这种方式定义函数, 函数 this 默认指向 undefined 也就是说 函数内部的 this 指向需要被更正
可以在构造函数中对函数的this 进行更正, 也可以在行内进行更正, 两者看起来没有太大的区别, 但是对性能的影响是不同的

  1. export default class App extends React.component {
  2. contructor() {
  3. super()
  4. /*
  5. * 方式 一
  6. * 构造函数只执行一次, 所以函数this 指向更正的代码也只执行一次
  7. */
  8. this.handleClick = this.handleClick.bind(this)
  9. }
  10. handleClick() {
  11. console.log(this)
  12. }
  13. render() {
  14. /*
  15. * 方式 二
  16. * 问题: render 方法每次执行时 都会调用 bind 方法生成新的函数实例
  17. */
  18. return <button onClick={this.handleClick.bind(this)}> 按钮 </button>
  19. }
  20. }

9、类组件中的箭头函数

在类组件中使用箭头函数不会存在 this 指向问题, 因为箭头函数本身并不绑定 this

  1. export default class App extends React.Component {
  2. handleClick = () => console.log(this)
  3. render() {
  4. return (
  5. <button onClick={this.handleClick}>按钮</button>
  6. )
  7. }
  8. }

箭头函数在this 指向问题上占据优势, 但是同时也有不利的一面
当使用箭头函数时, 该函数被添加为类的实例对象属性,而不是原型对象属性
如果组件被多次重用,每个组件实例对象中都会有一个相同的函数实例, 降低了函数实例的可重用性造成了资源浪费
综上所述,更正函数内部this 指向的最佳做法仍是在构造函数中使用 bind 方法进行绑定

10、避免使用内联样式属性

当使用内联 style 为元素添加时, 内联 style 会被编译为 javaScript 代码, 通过 javaScript 代码将样式规则映射到元素的身上, 浏览器就会花更多的时间执行脚本和渲染 UI, 从而增加了组件的渲染时间
他是在执行时为元素添加样式,而不是编译时为元素添加样式

  1. function App() {
  2. return <div style={{ backgroundColor: "blue" }}>
  3. App Works
  4. </div>
  5. }

在上面的组件中, 为元素附加了内联样式, 添加的内联样式为 javaScript 对象,backgroundColor 需要被转换为等效的 CSS 样式规则, 然后将其应用到元素, 这样涉及到脚本的执行
更好的办法是将 CSS 文件导入样式组件 能通过CSS 直接做的事情就不要通过 javaScript 去做,因为 javaScript 操作 DOM 非常慢

11、优化条件渲染

频繁的挂载和卸载组件是一项耗性能的操作,为了确保应用程序的性能, 应该减少组件挂载和卸载的次数
在React 中我们经常会根据条件渲染不同的组件, 条件渲染是一项必做的优化操作

  1. function App() {
  2. if (true) {
  3. return (
  4. <>
  5. <AdminHeader />
  6. <Header />
  7. <Content />
  8. </>
  9. )
  10. } else {
  11. return (
  12. <>
  13. <Header />
  14. <Content />
  15. </>
  16. )
  17. }
  18. }

在上面的代码中, 当渲染条件发生变化时,React 内部在做 Virtual DOM 比对时发现,刚刚第一个组件是 AdminHeader, 现在第一个组件是 Header 刚刚第二个组件是 Header 现在第二个组件是Content 组件发生了变化, React 就会卸载 AdminHeader、Header、Content, 重新挂载 Header 和 Content 这种挂载和卸载就是没有必要的

12、避免重复无限渲染

当应用程序状态发生变化时,React 会调用 render 方法, 如果在 render 方法中继续更改应用程序状态,就会发生 render 方法递归调用导致应用报错

  1. export default class App extends React.Component {
  2. constructor() {
  3. super()
  4. this.state = { name: "张三" }
  5. }
  6. render() {
  7. this.setState({ name: "李四" })
  8. return <div>{this.state.name}</div>
  9. }
  10. }

与其他声明周期函数不同, render 方法应该被作为纯函数,这意味着,在render 方法中不要做以下事情
比如 不要调用setState 方法
不要使用其他手段查询更改原生DOM 元素,
以及其他更改应用程序的任何操作,render 方法的执行要根据状态的改变,这样可以保持组件的行为和渲染方式一致

13、为组件创建错误边界

默认情况下,组件渲染错误会导致整个应用程序中断,创建错误边界可确保在特定组件发生错误时应用程序不会中断
错误边界是一个 React 组件,可以捕获子级组件在渲染时发生的错误,当错误发生时,可以将错误记录下来,可以显示备用 UI 界面
错误边界涉及到两个生命周期函数, 分别为 getDerivedStateFromError 和 componentDidCatch
getDerivedStateFromError 为静态方法, 方法中需要返回一个对象,该对象会和 state 对象进行合并,用于更改应用程序状态
componentDidCatch 方法用于记录应用程序错误信息,该方法的参数就是错误对象

  1. // ErrorBoundaries
  2. import React from "react"
  3. import App from "./App"
  4. export default class ErrorBoundaries extends React.Component{
  5. constructor(){
  6. super()
  7. this.state = {
  8. hasError: false
  9. }
  10. }
  11. componentDidCatch(error) {
  12. console.log('componentDidCatch')
  13. }
  14. static getDerivedStateFromError() {
  15. console.log('getDerivedStateFromError')
  16. return {
  17. hasError: true
  18. }
  19. }
  20. render() {
  21. if (this.state.hasError) {
  22. return <div>发生了错误</div>
  23. }
  24. return <App />
  25. }
  26. }

注意: 错误边界不能捕获异步错误,比如点击按钮时发生的错误

14、避免数据结构突变

组件中 props 和 state 的数据结构 应该保持一致, 数据结构突变会导致输出不一致

15、依赖优化

在应用程序中经常会依赖第三方包, 但我们不想引用包中的所有代码, 我们只想用到哪些代码就包含哪些代码 此时可以使用插件依赖项进行优化
1、下载依赖
npm install react-app-rewrited customize-cra lodash babel-plugin-lodash
2、在项目的根目录下新建 config-override.js 并加入配置代码

  1. const { override, useBabelRc } = require("customize-cra")
  2. moudule.exports = override(useBabelRc())

3、修改 package.json 文件中的构建命令

  1. "scripts": {
  2. "start": "react-app-rewrite start.js",
  3. "build": "react-app-rewrite build.js",
  4. "test": "react-app-rewrite test.js"
  5. },

4、创建 .babelrc 文件并加入配置

  1. {
  2. "plugins" : ["lodash"]
  3. }

会发现 打包后 的 体积 变小了