0


前端框架 react 性能优化

总览:react的优化核心思想就是让react跳过重新渲染那个些没有改变的Component,而只重新渲染发生变化的Component。

所以变化都是围绕这三种变量展开的:props、state、context 。

一、不使用任何性能优化API进行优化

核心思想:将变和不变的部分分离开

案例一:没有变化的组件被多次渲染了

  1. function App() {
  2. const [num, updateNum] = useState(0)
  3. return (<div className="App">
  4. <input type="text" onChange={(e) => updateNum(+e.target.value)}/>
  5. <p>num is {num}</p>
  6. <ExpensiveCpn/>
  7. </div>);
  8. }
  9. const ExpensiveCpn = () => {
  10. let now = performance.now()
  11. while (performance.now() - now < 100) {
  12. }
  13. console.log("组件耗时 render")
  14. return <div>耗时组件</div>
  15. }

运行结果:可以看到,在上面代码的情况下,高耗能组件虽然没有被修改,但每次修改state里的数据还是都会重新渲染改组件。

下面对其进行优化,即变的部分组件抽离:

  1. function App() {
  2. return (<div className="App">
  3. <Input/>
  4. <ExpensiveCpn/>
  5. </div>);
  6. }
  7. const Input = () => {
  8. const [num, updateNum] = useState(0)
  9. return (<>
  10. <input type="text" onChange={(e) => updateNum(+e.target.value)}/>
  11. <p>num is {num}</p>
  12. </>)
  13. }
  14. const ExpensiveCpn = () => {
  15. let now = performance.now()
  16. while (performance.now() - now < 100) {
  17. }
  18. console.log("组件耗时 render")
  19. return <div>耗时组件</div>
  20. }

页面效果:我们将页面变的部分和不变的部分进行抽离后实现效果:变的组件发生变化时,不会影响我们的其他组件了。

如果你的代码结果写的好,你几乎可以不用性能优化API。

案例二:与案例一类似,不同在于这里父组件也用到了state里的变量。

  1. function App() {
  2. const [num, updateNum] = useState(0)
  3. return (<div className="App" title={num + ""}>
  4. <input type="text" onChange={(e) => updateNum(+e.target.value)}/>
  5. <p>num is {num}</p>
  6. <ExpensiveCpn/>
  7. </div>);
  8. }
  9. const ExpensiveCpn = () => {
  10. let now = performance.now()
  11. while (performance.now() - now < 100) {
  12. }
  13. console.log("组件耗时 render")
  14. return <div>耗时组件</div>
  15. }

这段代码和案例一没有优化前的效果一样。

下面我们将对其进行优化:

  1. function App() {
  2. return (<InputWrapper>
  3. <ExpensiveCpn/>
  4. </InputWrapper>);
  5. }
  6. function InputWrapper({children}) {
  7. const [num, updateNum] = useState(0)
  8. return (
  9. <div className="App" title={num + ""}>
  10. <input type="text" onChange={(e) => updateNum(+e.target.value)}/>
  11. <p>num is {num}</p>
  12. {children}
  13. </div>
  14. )
  15. }
  16. const ExpensiveCpn = () => {
  17. let now = performance.now()
  18. while (performance.now() - now < 100) {
  19. }
  20. console.log("组件耗时 render")
  21. return <div>耗时组件</div>
  22. }

页面效果:可以看到通过{children}插槽可以实现父元素也有变量时的优化

从以上两个案例可以得出以下结论:

当父组件满足性能优化条件时,子孙组件可能命中性能优化。

本质是:将变的部分和不变的部分分离

二、通过性能优化API优化

为什么需要性能优化API:

当子孙结点的父结点未命中性能优化时,父结点的分支也将不会命中性能优化。

1、React.memo

官方文档:memo – React 中文文档

React.memo是一个高阶组件,它接收另一个组件作为参数,会返回一个包装过的新组件,包装后的新组件就会具有缓存作用。

包装后,只有组件的props发生变化,才会触发组件的重新渲染,否则总是返回缓存中的结果。

案例一:

  1. function App() {
  2. console.log("App发生渲染")
  3. const [count, setCount] = useState(1)
  4. return (
  5. <>
  6. <div className="App">
  7. <Son/>
  8. <p>{count}</p>
  9. <button onClick={() => setCount(count => ++count)}>点击加1</button>
  10. </div>
  11. </>
  12. );
  13. }
  14. const Son = () => {
  15. console.log("Son发生渲染")
  16. return <div>
  17. <p>son</p>
  18. <GrandSon/>
  19. </div>
  20. }
  21. const GrandSon = () => {
  22. console.log("GrandSon发生渲染")
  23. return <div><p>GrandSon</p></div>
  24. }

实现效果:每次修改App里的数据,子孙组件都发生了改变。

案例二:

对案例一进行优化:

  1. function App() {
  2. console.log("App发生渲染")
  3. const [count, setCount] = useState(1)
  4. return (
  5. <>
  6. <div className="App">
  7. <Son/>
  8. <p>{count}</p>
  9. <button onClick={() => setCount(count => ++count)}>点击加1</button>
  10. </div>
  11. </>
  12. );
  13. }
  14. const Son = React.memo(() => {
  15. console.log("Son发生渲染")
  16. return <div>
  17. <p>son</p>
  18. <GrandSon/>
  19. </div>
  20. })
  21. const GrandSon = () => {
  22. console.log("GrandSon发生渲染")
  23. return <div><p>GrandSon</p></div>
  24. }

实现效果:可以看到,当Son组件被memo包裹时,Son一系列数下的组件都被缓存优化到了。每次修改父组件,子孙组件都未重新渲染。

** 案例三:**

在App组件中,像子组件Son传入修改参数的setCount函数。

  1. function App() {
  2. console.log("App发生渲染")
  3. const [count, setCount] = useState(1)
  4. const addOne = () => {
  5. setCount(count => ++count)
  6. }
  7. return (
  8. <>
  9. <div className="App">
  10. <Son addOne={addOne}/>
  11. <p>{count}</p>
  12. <button onClick={addOne}>点击加1</button>
  13. </div>
  14. </>
  15. );
  16. }
  17. const Son = React.memo(({addOne}) => {
  18. console.log("Son发生渲染")
  19. return <div>
  20. <p>son</p>
  21. <button onClick={addOne}>Son点击加1</button>
  22. <GrandSon/>
  23. </div>
  24. })
  25. const GrandSon = () => {
  26. console.log("GrandSon发生渲染")
  27. return <div><p>GrandSon</p></div>
  28. }

实现效果:当Son组件中调用了父组件中的函数时,你会发现React.memo失效了,此时,子孙组件仍然会被重新渲染。因为App组件发生改变重新渲染,addOne函数也会重新定义,此时传入Son组件里的函数就相当于更新了,会使得Son进行重新渲染。但问题是Son里的内容并没有改变,有什么方法可以解决这种情况的问题呢?这就要用到下面介绍的useCallback钩子了。

2、useCallback

useCallback是一个钩子函数,用来创建React中的回调函数。创建的回调函数不会总在组件重新渲染时重新创建。简单来说,就是对回调函数做了一层缓存。

  1. const cachedFn = useCallback(fn, dependencies)

useCallback的第一个参数是一个回调函数,第二个参数是依赖数组(当依赖数组中的变量发生变化时,回调函数才会重新创建;如果不指定依赖数组,回调函数每次都会重新创建,失去意义)。

注:如果使用时,不传第二个参数,函数仍然会在每次渲染时重新创建,和没使用没什么区别。

案例四:

  1. function App() {
  2. console.log("App发生渲染")
  3. const [count, setCount] = useState(1)
  4. const addOne = useCallback(() => {
  5. setCount(count => ++count)
  6. },[]) // 这里的函数只会在组件初始化时创建,更新时不会再次创建
  7. return (
  8. <>
  9. <div className="App">
  10. <Son addOne={addOne}/>
  11. <p>{count}</p>
  12. <button onClick={addOne}>点击加1</button>
  13. </div>
  14. </>
  15. );
  16. }
  17. const Son = React.memo(({addOne}) => {
  18. console.log("Son发生渲染")
  19. return <div>
  20. <p>son</p>
  21. <button onClick={addOne}>Son点击加1</button>
  22. <GrandSon/>
  23. </div>
  24. })
  25. const GrandSon = () => {
  26. console.log("GrandSon发生渲染")
  27. return <div><p>GrandSon</p></div>
  28. }

实现效果:React.memo案例三中的问题解决了

案例五:

  1. function App() {
  2. console.log("App发生渲染")
  3. const [count, setCount] = useState(1)
  4. const [num, setNum] = useState(1)
  5. const addOne = useCallback(() => {
  6. setCount(count => count + num)
  7. setNum(num => num + 1)
  8. }, [num])
  9. return (
  10. <>
  11. <div className="App">
  12. <Son addOne={addOne}/>
  13. <p>{count}</p>
  14. <button onClick={addOne}>点击加1</button>
  15. </div>
  16. </>
  17. );
  18. }
  19. const Son = React.memo(({addOne}) => {
  20. console.log("Son发生渲染")
  21. return <div>
  22. <p>son</p>
  23. <button onClick={addOne}>Son点击加1</button>
  24. <GrandSon/>
  25. </div>
  26. })
  27. const GrandSon = () => {
  28. console.log("GrandSon发生渲染")
  29. return <div><p>GrandSon</p></div>
  30. }

实现效果:在依赖数组中数组callback函数中使用到的变化,每当数组中的变量发生改变时,回调函数都会重新定义,执行时机和useEffect依赖数组里是类似的。但案例中这种情况已经失去了优化的初衷。所以,案例五中的情况,尽量不要用useCallback了,使用和不使用效果都一样,还多了一层缓存的耗时操作。

3、useMemo

相当于Vue里的Computed

与useCallback十分相似,useCallback是用来缓存函数对象,useMemo是用来缓存函数的执行结果。

不使用useMemo的执行效果:

  1. const sum = (a, b) => {
  2. console.log("求和执行了")
  3. return a + b
  4. }
  5. function App() {
  6. console.log(sum(1,2))
  7. console.log(sum(1,2))
  8. return (
  9. <>
  10. <div></div>
  11. </>
  12. );
  13. }

效果:每次调用都会重新执行。

使用了useMemo:

  1. const sum = (a, b) => {
  2. console.log("求和执行了")
  3. return a + b
  4. }
  5. function App() {
  6. const result = React.useMemo(() => {
  7. return sum(1, 2)
  8. })
  9. console.log(result)
  10. console.log(result)
  11. return (
  12. <>
  13. <div></div>
  14. </>
  15. );
  16. }

效果:函数只执行了一次,对于开销很大的函数,使用useMemo可以很好地改善性能。

** 当useMemo里传入变量时:**

  1. const sum = (a, b) => {
  2. console.log("求和执行了")
  3. return a + b
  4. }
  5. function App() {
  6. const [count, setCount] = useState(1)
  7. let b = 2
  8. // 每次组件渲染时,都会执行
  9. // useMemo用于缓存函数的执行结果
  10. const result = React.useMemo(() => {
  11. return sum(count, b)
  12. }, [])
  13. useEffect(() => {
  14. console.log("count:", count)
  15. console.log(result)
  16. console.log(result)
  17. }, [count])
  18. return (
  19. <>
  20. <div>
  21. <button onClick={() => setCount(prev => prev + 1)}>a1</button>
  22. </div>
  23. </>
  24. );
  25. }

打印:可以看到每次执行都是第一次的结果,变量改变也没有重新缓存result。

解决上面问题的方法:

const result = React.useMemo(() => {
return sum(count, b)
}, [count]) // 第二个参数数组中传入对应变量,当变量发生变化时,会重新调用useMemo进行结果缓存更新。

对于上述情况,如果修改过于频繁,就基本使用不到缓存效果,这种情况,不推荐使用useMemo。

另外,useMemo也可以像React.memo一样返回组件缓存:

  1. function App() {
  2. const el = useMemo(() => {
  3. return <div><p>hello</p></div>
  4. }, [])
  5. return (
  6. <>
  7. <div>
  8. {el}
  9. </div>
  10. </>
  11. );
  12. }
4、PureComponent

React.memo的对应。

PureComponent 会对 props 和 state 进行浅层比较。如果它们没有变化,组件将不会重新渲染。
示例:
以下是一个在类组件中使用 PureComponent 的示例,包括数据传递和更新:

  1. import React, { PureComponent } from 'react';
  2. class MyComponent extends PureComponent {
  3. // 构造函数,初始化状态
  4. constructor(props) {
  5. super(props);
  6. this.state = {
  7. count: 0,
  8. name: 'Initial Name',
  9. };
  10. }
  11. // 处理点击事件,更新状态
  12. handleClick = () => {
  13. // 示例 1:更新数字状态
  14. this.setState({ count: this.state.count + 1 });
  15. // 示例 2:更新字符串状态(如果 name 是从父组件传递的 props 且未变化,不会触发重新渲染)
  16. // 假设 name 是从父组件传递的 props,以下更新不会触发重新渲染(如果 name 未变化)
  17. // this.setState({ name: this.props.name });
  18. };
  19. render() {
  20. return (
  21. <div>
  22. <p>Count: {this.state.count}</p>
  23. <p>Name: {this.state.name}</p>
  24. <button onClick={this.handleClick}>Increment Count</button>
  25. </div>
  26. );
  27. }
  28. }
  29. // 父组件
  30. class ParentComponent extends React.Component {
  31. constructor(props) {
  32. super(props);
  33. this.state = {
  34. name: 'Parent Name',
  35. };
  36. }
  37. handleNameChange = () => {
  38. this.setState({ name: 'Updated Name' });
  39. };
  40. render() {
  41. return (
  42. <div>
  43. <MyComponent name={this.state.name} />
  44. <button onClick={this.handleNameChange}>Change Name</button>
  45. </div>
  46. );
  47. }
  48. }
  49. export default ParentComponent;

在这个例子中:

MyComponent 是一个继承自 PureComponent 的类组件。它有一个 count 状态用于数字的递增展示,还有一个 name 状态(也可以是从父组件传递的 props)用于展示字符串。

在 render 方法中,展示了 count 和 name 的值,并有一个按钮用于触发 count 的递增。

ParentComponent 是父组件,它有一个 name 状态,并将其传递给 MyComponent。还有一个按钮用于更改 name 的状态。

PureComponent 会对 props 和 state 进行浅层比较。如果 props 或 state 的引用没有变化,组件将不会重新渲染。在上面的例子中,如果 MyComponent 接收到的 props.name 没有变化,并且 state 中的 count 没有更新,MyComponent 就不会重新渲染。

注意事项:

PureComponent 的浅层比较对于基本数据类型(如数字、字符串、布尔值)是有效的,但对于复杂数据类型(如对象、数组),它只会比较引用。如果对象或数组的内容发生变化,但引用不变,PureComponent 可能不会检测到变化。在这种情况下,可以使用 immutable.js 或手动在 shouldComponentUpdate 中进行深层比较。
如果组件的 props 或 state 变化频繁且计算成本不高,或者需要进行深层比较,可能不需要使用 PureComponent。

三、总结​

性能优化三部曲:

1、寻找项目中性能损耗严重的子树

2、在子树的根节点使用性能优化API;

3、子树中运用变与不变分离原则。

总结到此,相信你已经掌握了性能优化的精髓。


本文转载自: https://blog.csdn.net/qq_50909707/article/details/143878878
版权归原作者 Dragon Wu 所有, 如有侵权,请联系我们删除。

“前端框架 react 性能优化”的评论:

还没有评论