React的性能问题和优化

297 阅读8分钟

React的性能问题

React最大的一个性能问题:React的某个组件的更新(state状态和props属性更新都会让组件更新)会连带着它的子组件一起更新。

解决这个问题的方法:

  1. 源码上尽量弥补这个问题(React的时间切片)
  2. 让子组件只做合理的更新

React的时间切片

Vue有依赖收集,做到了最小的更新范围。而React并没有做依赖收集,所以React要更新的话会更新整个组件树,这样就会有很大的diff算法对比和计算工作。如果有大量的更新操作,虚拟dom对比和计算就会花很大的时间,这样可能会阻塞浏览器的渲染,导致页面长时间白屏。

React为了解决这个问题选择的策略就是时间切片。时间切片就是先计算一部分的更新,然后让渲染进程进行渲染,然后再进行下一步更新。如此往复,就不会出现长时间的白屏了。

React会保障在执行更新操作时计算的时间不能超过16ms,如果超过16ms就需要先暂停,让给浏览器进行渲染。

image.png

为了支持这种切片,需要将更新转化为一个一个的单元,然后也必须要有恢复上一次中断的计算进度的能力。React就设计了一种数据结构——fiber,每一个组件都会被转化为fiber结构的对象,然后组成一个一个的单元,fiber让React有恢复上一次中断的计算进度的能力。

image.png

在组件树中每个组件单元都有child指针指向当前组件的子组件和sibling指针指向当前组件的兄弟组件。

无用渲染的问题和解决方法

父组件state状态的数据更改导致子组件更新

父组件数据的更改导致父组件进行更新操作,而父组件的更新会连带着它的子组件一起进行更新。

// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx"; // 引入子组件
export default function App() {
  console.log("App父组件渲染了")
  let [num, setNum] = useState(0);
  const numChange = () => {
    setNum(++num);
  }
  return (
    <div>
      <h1>App父组件</h1>
      <p>{num}</p>
      <button onClick={numChange}>点击加1</button>
      <Son />
    </div>
  )
}
// Son.jsx
import React from "react";
const Son = (props) => {
    console.log("Son组件渲染了")
    return (
        <>
            <h2>Son组件</h2>
        </>
    )
}
export default Son;

2.gif

在父组件中更新了父组件state状态的num数据,导致父组件更新了。这里没有修改子组件的state状态,按理来说子组件不会更新,但父组件的更新导致了子组件的更新。

解决方法:在类组件中可以使用PureComponent来避免父组件数据更改导致子组件更新,在函数组件中使用React.Memo避免父组件数据更改导致子组件更新。

// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx"; // 引入子组件
export default function App() {
  console.log("App父组件渲染了")
  let [num, setNum] = useState(0);
  const numChange = () => {
    setNum(++num);
  }
  return (
    <div>
      <h1>App父组件</h1>
      <p>{num}</p>
      <button onClick={numChange}>点击加1</button>
      <Son />
    </div>
  )
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
    console.log("Son组件渲染了")
    return (
        <>
            <h2>Son组件</h2>
        </>
    )
}
// memo方法接受一个组件,它会返回一个新的组件。memo实际上就是一个高阶组件
const MemoSon = memo(Son);
export default MemoSon;

2.gif

组件的state状态修改为同样的值而产生的更新

解决方法:类组件的PureComponent以及函数组价本身会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。

// App.jsx
import React from "react"
import Des from "./Des.jsx";
export default function App() {
  console.log("App组件渲染了");
  return (
    <div>
      <Des />
    </div>
  )
}
// Des.jsx
import React from "react"
class Des extends React.Component {
    state = {
        msg: "您收到一条消息"
    }
    changeMsg = () => {
        this.setState({ msg: "您收到一条消息" })
    }
    render() {
        console.log("Des组件渲染了");
        return (
            <>
                <h1>Des组件</h1>
                <p>{this.state.msg}</p>
                <button onClick={this.changeMsg}>修改</button>
            </>
        )
    }
}
export default Des

2.gif

Des类组件的state状态的msg数据修改的值都是同一个,但是组件还是更新了。

解决方法:类组件继承于PureComponent组件,而非继承于Component组件。PureComponent组件会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。

// App.jsx
import React from "react"
import Des from "./Des.jsx";
export default function App() {
  console.log("App组件渲染了");
  return (
    <div>
      <Des />
    </div>
  )
}
// Des.jsx
import React from "react"
class Des extends React.PureComponent {
    state = {
        msg: "您收到一条消息"
    }
    changeMsg = () => {
        console.log("修改方法执行了");
        this.setState({ msg: "您收到一条消息" })
    }
    render() {
        console.log("Des组件渲染了");
        return (
            <>
                <h1>Des组件</h1>
                <p>{this.state.msg}</p>
                <button onClick={this.changeMsg}>修改</button>
            </>
        )
    }
}
export default Des

2.gif

函数组价本身会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。

// App.jsx
import React from "react"
import Son from "./Son.jsx";
export default function App() {
  console.log("App组件渲染了");
  return (
    <div>
      <Son />
    </div>
  )
}
// Son.jsx
import React, { useState } from "react";
const Son = () => {
    console.log("Son组件渲染了");
    let [msg, setMsg] = useState("您收到一条消息");
    const changeHandle = () => {
        console.log("修改方法执行了");
        setMsg("您收到一条消息");
    }
    return (
        <>
            <h1>Son组件</h1>
            <p>{msg}</p>
            <button onClick={changeHandle}>修改</button>
        </>
    )
}
export default Son

2.gif

父组件props属性传入方法、对象、数组这些引用类型时而导致的子组件的更新

即使类组件中使用PureComponent和函数组件中使用React.Memo来避免了父组件state状态的数据更改导致子组件更新,以及做到了如果父组件传入的props属性不改变就不会触发子组件的更新。但是只要父组件传入子组件的props的引用发生变化,子组件就会重新渲染。

这是因为在React中,当父组件向子组件传递引用类型的props(如对象、数组、方法)时,React通过浅比较来检测props的变化,如果引用不同,React会认为传入子组件的props发生了变化,从而触发子组件的更新。

以下是一些常见的情况,子组件可能因为新的props引用而更新:

  • 每次渲染都创建新对象或数组:

如果父组件在每次渲染时都创建一个新的对象或数组,并将其作为props传递给子组件,那么即使这些对象或数组的内容与上次相同,但由于它们是不同的引用,React会认为props已改变并重新渲染子组件。

  • 使用状态管理库时的不当操作:

在使用如Redux等状态管理库时,如果状态树中的某个部分被替换为一个新对象(即使内容相同),这将导致所有订阅该状态的组件接收到新的引用,从而触发重新渲染。

  • 函数组件的useStateuseReducer

当使用useStateuseReducer等Hook时,如果状态是一个对象或数组,并且状态更新函数返回了一个新的引用,即使这个新的引用与旧的引用内容完全相同,子组件仍然会被重新渲染。

举例其中一个说明:

// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx";
export default function App() {
  console.log("App组件渲染了");
  let [num,setNum] = useState(0);
  const addChange = () => {
    setNum(num++);
  }
  let obj = {
    title:"Earth Sun"
  }
  let fn = () => {
    console.log(123);
  }
  return (
    <div>
      <h1>App组件</h1>
      <p>{num}</p>
      <button onClick={addChange}>点我加1</button>
      <Son fn={fn} obj={obj} />
    </div>
  )
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
    console.log(props,99)
    console.log("Son组件渲染了");
    return (
        <>
            <h2>Son组件</h2>
            <p>{props.obj.title}</p>
        </>
    )
}
const MemoSon = memo(Son)
export default MemoSon

2.gif

App组件中修改了App组件的state状态的num数据,让App组件的函数重新运行了,从而让obj对象和fn方法这两个引用数据被重新创建了,它们的引用地址发生了改变,所以传入Son组件的props发生了改变,也就让Son组件重新渲染了。

解决方法:用useCallback包裹传递给子组件的方法,传给子组件的非state状态的对象和数组的数据要用useMemo包裹起来。useCallback用于缓存一个方法让这个方法不会被重新创建,useMemo用于缓存一个数据让这个数据不会被重新创建。因为useCallbackuseMemo这两个方法开销也很大,大量使用也会有性能问题,所以推荐只有给子组件的方法,值用它们包裹起来。

// App.jsx
import React, { useState, useCallback, useMemo } from "react"
import Son from "./Son.jsx";
export default function App() {
  console.log("App组件渲染了");
  let [num, setNum] = useState(0);
  const addChange = () => {
    setNum(num++);
  }
  // 传入空数组,只在页面初次加载时会执行第一个参数的函数体,创建这个对象数据
  let obj = useMemo(() => {
    return {
      title: "Earth Sun"
    }
  },[])
  // 传入空数组,只在页面初次加载时会执行第一个参数的函数体,创建这个方法数据
  let fn = useCallback(() => {
    console.log(123);
  }, [])
  return (
    <div>
      <h1>App组件</h1>
      <p>{num}</p>
      <button onClick={addChange}>点我加1</button>
      <Son fn={fn} obj={obj} />
    </div>
  )
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
    console.log(props,99)
    console.log("Son组件渲染了");
    return (
        <>
            <h2>Son组件</h2>
            <p>{props.obj.title}</p>
        </>
    )
}
const MemoSon = memo(Son)
export default MemoSon

2.gif