react性能优化

167 阅读6分钟

减少不必要的渲染

合理使用shouldComponentUpdate

class App extends React.Component {
  
  state = {
    count: 1,
  };

  shouldComponentUpdate(nextProps, nextState) {
    if(nextState.count === this.state.count) {
      return false
    }
    return true
  }

  click = () => {
    this.setState({ count: 1 });
  };

  render() {
    console.log('--------render')
    return <button onClick={this.click}>{this.state.count}</button>;
  }
}

合理使用PureComponent

PureComponentshouldComponentUpdate中实现了浅比较(比较最外层属性)

class App extends React.PureComponent {

  state = {
    count: 1,
  };

  click = () => {
    // this.setState({ count: this.state.count+1 });
    this.setState({count: 1})
  };

  render() {
    console.log('--------render')
    return <button onClick={this.click}>{this.state.count}</button>;
  }
}

PureComponent原理

class App extends React.Component {

  state = {
    count: 1,
  };

  shouldComponentUpdate(nextProps, nextState) {
    const hasNotChange = 
        objEqual(this.props, nextProps) && 
        objEqual(this.state, nextState)
        
    if(hasNotChange === true){
      return false
    }
    return true
  }

  click = () => {
    // this.setState({ count: this.state.count+1 });
    this.setState({count: 1})
  };

  render() {
    console.log('--------render')
    return <button onClick={this.click}>{this.state.count}</button>;
  }
}

// 浅比较
function objEqual(oldObj, newObj) {
  for(let key in newObj) {
    if(oldObj[key] !== newObj[key]) {
      return false
    }
  }
  return true
}

合理使用memo

函数组件用memo,实现和 pureComponent一样的功能

问题:

  • 父组件更新, 子组件也触发了更新
  • 但是子组件的props并没有发生变化, 并不需要重新渲染
import React, { useState, useCallback } from 'react@18';
import { createRoot } from 'react-dom@18/client';

const Test = function () {
  const [count1, setCount1] = useState<number>(0);

  const handleClick = () => {
    setCount1(count1 + 1);
  }

  return <div onClick={handleClick}>
    <h1>click me {count1}</h1>
    <Child />
  </div>;
};
      
let Child = (props) => {
  alert('-----render')
  return <h1>
    {props.count}
  </h1>
}

解决: 使用Reat.memo

import React, { useState, useCallback } from 'react@18';
import { createRoot } from 'react-dom@18/client';

const Test = function () {
  const [count1, setCount1] = useState<number>(0);

  const handleClick = () => {
    setCount1(count1 + 1);
  }

  return <div onClick={handleClick}>
    <h1>click me {count1}</h1>
    <Child />
  </div>;
};    

let Child = (props) => {
  alert('-----render')
  return <h1>
    {props.count}
  </h1>
}

Child = React.memo(Child) // <---------------

原理

function memo(FnComponent) {
    return class Memo extends React.PurComponent {
        render(){
            return <>{FnComponent(this.props)}</>
        }
    }
}

useMemo缓存数据

  • 如果依赖的数据经常变化,就不用useMemo
  • 如果依赖的数据偶尔变化,就可以用useMemo

问题: 每次修改n1, getN3 就会执行一次

const Test = function () {
  const [n1, setN1] = useState(0)
  const [n2] = useState(0)

  const click = () => setN1(n1+1)

  const getN3 = () => {
    alert('getN2执行了')
    return n2+100
  }
  
  return (
    <>
      <h1 onClick={click} >{n1}</h1>
      <h1>{getN3()}</h1>
    </>
  )
};

解决: 使用useMemo

const Test = function () {
  const [n1, setN1] = useState(0)
  const [n2] = useState(0)

  const click = () => setN1(n1+1)

  const n3 = useMemo(() => {
    return n2+100
  }, [n2])
  
  return (
    <>
      <h1 onClick={click} >{n1}</h1>
      <h1>{n3}</h1>
    </>
  )
};

问题: React.memo 在下面情况无法阻止子组件发生渲染

const Test = function () {
  const [n1, setN1] = useState(0)
  const [n2, setN2] = useState(18)

  const click = () => setN1(n1+1)
  const jack = {age: n2}
  
  return (
    <>
      <h1 onClick={click} >{n1}</h1>
      <Child a={jack} />
    </>
  )
};

let Child = (props) => {
  alert('---render')
  return <h1>child: {props.a.age}</h1>
}
Child = React.memo(Child)

解决: 使用useMemo

const Test = function () {
  const [n1, setN1] = useState(0)
  const [n2, setN2] = useState(18)

  const click = () => setN1(n1+1)

  const jack = useMemo(() => {
    return {age: n2}
  }, [n2])
  
  return (
    <>
      <h1 onClick={click} >{n1}</h1>
      <Child a={jack} />
    </>
  )
};

let Child = (props) => {
  alert('---render')
  return <h1>child: {props.a.age}</h1>
}
Child = React.memo(Child)

useCallback 缓存函数

  • 如果依赖值经常变化, 使用useCallback就没有意义

问题: 即使使用了React.memo, 父组件更新, 子组件也会更新

import { memo, useCallback, useMemo, useState } from "react";

let Child = ({ info }) => {
  console.log("---child render");
  return <h3>child: {info.name}</h3>;
}
// 子组件使用了 memo 进行浅比较
Child = memo(Child) // <<--------

export default function App() {
  const [num, setNum] = useState(0);

  const cb = () => {}

  return (
    <div className="App">
      <h1 onClick={() => setNum(num + 1)}>{num}</h1>
      <Child cb={cb} />
    </div>
  );
}

使用useCallback解决了上面问题

import { memo, useCallback, useMemo, useState } from "react";

let Child = ({ info }) => {
  console.log("---child render");
  return <h3>child: {info.name}</h3>;
}
// 子组件使用了 memo 进行浅比较
Child = memo(Child) // <<--------

export default function App() {
  const [num, setNum] = useState(0);

  const cb = useCallback(() => {}, []);

  return (
    <div className="App">
      <h1 onClick={() => setNum(num + 1)}>{num}</h1>
      <Child cb={cb} />
    </div>
  );
}

减少层级

使用Fragment

return <>
    <div>xxx</div>
</>

避免多层级的嵌套组件

  • 当组件层级嵌套过深时,会增加diff算法的复杂度和渲染时间。尽量保持组件的层级扁平和简洁。
  • 就是尽量减少fiber节点的数量

class 组件

在构造函数中bind(this)

good

class MyComponent extends React.Component {

    constructor(){
        this.click = this.click.bind(this)
    }
    
    click(){
        alert(1111)
    }
    
    render(){
        return <Button onClick={this.click}>按钮</Button>
    }
}

bad

  • 如果这样写
  • 数据每更新一次, render就执行一次
  • render每执行一次, bind就会执行一次
  • 上面的写法不会频繁的执行bind
render(){
  return <Button onClick={this.click.bind(this)}>按钮</Button>
}

jsx中不要定义函数

bad

  • 数据每更新一次, render就执行一次
  • render每执行一次, 函数就会被定义一次
<Button onClick={() => {...}}>按钮</Button>

good

  • 数据更新, render方法会不停的执行
  • 函数写在外面, 避免被不停创建
class MyComponent extends React.Component {
    click = () => {...}
    
    render(){
        return <Button onClick={this.click}>按钮</Button>
    }
}

模拟v-show

// v-show
<MyComponent style={{display: flag ? 'block' : 'none'}} />

// v-if
{show && <MyComponent />}

useState优化

  • 可以在useState中传入函数提升性能
// ❌ 错误:每次渲染都会执行 calculateInitialValue()
const [value] = useState(calculateInitialValue());

// ✅ 正确:只在组件挂载时执行一次
const [value] = useState(() => {
  return calculateInitialValue(); // 复杂计算
});

Suspense & 懒加载

对于不是首次加载就需要的组件,可以采用懒加载的方式,按需加载它们,减少初始渲染体积和提高首屏加载速度。在React中,可以使用Suspenselazy组件来实现。

  • 使用 React.Lazyimport() 来引入组件
  • 使用<React.Suspense></React.Suspense>来做异步组件的父组件,并使用 fallback 来实现组件未加载完成时展示信息
  • fallback 可以传入html,也可以自行封装一个统一的提示组件

const Child = lazy(
    /* webpackChunkName: 'Chidl'*/
    () => import("./child")
);

export default function App() {
  return (
    <h1 className="App">
      <Suspense fallback={<h1>loading...</h1>}>
        <Child msg="aaa" />
      </Suspense>
    </h1>
  );
}

自定义事件dom事件及时销毁


export default function App() {
  const [w, setW] = useState(0);

  function getW(e: UIEvent) {
    setW(e.target.innerWidth);
  }

  useEffect(() => {
    window.addEventListener("resize", getW);

    return () => {
      window.removeEventListener("resize", getW);
    };
  }, []);

  return <h1 className="App">{w}</h1>;
}

列表渲染性能优化key

  • 组件是否更新条件:
    • props发生变化
    • state发生变化
  • 不加key: [1,2,3] -> [4,1,2,3]
    • 1 更新成 4
    • 2 更新成 1
    • 3 更新成 2
    • 重新创建3添加到尾部
  • 加key: [1,2,3] -> [4,1,2,3]
    • 1, 2, 3, 都不变
    • 创建4添加到头部

const Child = memo<{ msg: number }>(({ msg }) => {
  console.log(msg, "---child render");
  return <h3>child {msg}</h3>;
});

export default function App() {
  const [arr, setArr] = useState([1, 2, 3]);
  // 添加到前面: 如果不加key 所有的Chid都会重新渲染
  const addHead = () => setArr([Math.random(), ...arr]);
  // 添加到后面: 加不加key都不影响
  const addEnd = () => setArr([...arr, Math.random()]);

  return (
    <div className="App">
      <p onClick={addHead}>添加到前面: 如果不加key 所有的Chid都会重新渲染</p>
      <p onClick={addEnd}>添加到后面: 加不加key都不影响</p>

      {arr.map((num) => (
        <Child msg={num} key={num} />
      ))}
    </div>
  );
}

合理使用Immutable.js

在React中使用Immutable数据可以减少渲染的次数。Immutable数据在更新时会产生新的对象,而不是修改原对象,这有助于在shouldComponentUpdate中进行浅比较,从而避免不必要的渲染。

类似的库还有immer

const jack = Immutable.Map({
  name: "jack",
  age: 22
});
const tom = jack.set("name", "tom");

jack.get("name"); // jack
tom.get("name"); // tom

拆分

  • 第三方库的代码基本上是不会变的
  • 可以将他们分包出来缓存到本地
  • 第二次加载时直接从本地读取
    • 分析打包结果
    • 路由懒加载
    • 将react-dom单独打一个包
    • 将antd单独打一个包
    • 将其他第三方库单独打一个包
    • 可以将包从1.7M优化到30kb

其他

除了上述React特有的优化策略外,还可以考虑一些通用的前端性能优化手段,如资源加载优化、减少重绘与回流、服务端渲染、启用CDN等。这些策略同样适用于React应用,可以进一步提升应用的性能。

需要注意的是,性能优化是一个持续的过程,需要根据应用的实际情况和性能瓶颈进行针对性的优化。同时,也要注意不要过度优化,以免引入不必要的复杂性和维护成本。