React性能优化的几种方式及思考

414 阅读8分钟

一旦父组件渲染,所有子组件都要跟着渲染,尽管这个子组件并没有任何改变。在这种情况下,这个子组件的渲染就变得多余。下面是几种处理方式:

shouldComponentUpdate

ShouldComponentUpdate生命周期函数内部通过props和state的浅比较来决定是否需要渲染,如果之前的prevProp和prevState跟当前的props,state 浅比较相同的话,就会返回false,组件就不会进行渲染。

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

React.pureComponent

使用PureComponent会帮你内置一个ShouldComponentUpdate的生命周期, 例:

class Parent extends React.Component {
  state = {
    person: { name: '张三' },
    name: 'hh',
  };

  componentDidMount() {
    setTimeout(() => {
      this.setState({ person: { name: '张三' }, name: 'hh' });
      this.handleChange();
    }, 3000);
  }

  handleChange = () => {};

  render() {
    return (
      <Child
        name={this.state.name} // 不会引起子组件改变
        person={this.state.person} // 会引起改变
        todo={this.handleChange} // 不会引起改变
      />
    );
  }
}

class Child extends React.PureComponent {
  render() {
    console.log('dfddddd');
    return <div>{this.props.name}</div>;
  }
}

export default Parent;  

如果父组件是函数组件,可以使用Memo配合useCallback钩子函数,来避免重复渲染。 使用memo函数包裹子组件,而在使用函数的情况,需要考虑有没有函数传递给子组件使useCallback。

import Child from './child.tsx';

const App = () => {
  const [state,setState] = useState({})
  const handleTimerPickerChange = useCallback((hour: number, minute: number) => {
    //复杂处理....
    ...
    setState(s=>({a:a+1}))
  }, []);

  return (
  ...
    <Child
      onChange={handleTimerPickerChange}
    />
  )
}

export default App

const Child:React.FC<> = () => {
  return (
    ...
  )
}

export default React.memo(Child)

避免使用内联对象

使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于prop的浅层比较始终返回false,导致组件一直重新渲染。
可以利用ES6扩展运算符将传递的对象解构。这样组件接收到的便是基本类型的props,组件通过浅层比较发现接受的prop没有变化,则不会重新渲染。示例如下:

// Don't do this!
function Component(props) {
  const aProp = { someProp: 'someValue' }
  return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />  
}

// Do this instead
const styles = { margin: 0 };
function Component(props) {
  const aProp = { someProp: 'someValue' }
  return <AnotherComponent style={styles} {...aProp} />  
}

避免使用匿名函数

虽然匿名函数是传递函数的好方法(特别是需要用另一个prop作为参数调用的函数),但它们在每次渲染上都有不同的引用。

// 避免这样做
function Component(props) {
  return <AnotherComponent onChange={() => props.callback(props.id)} />  
}

// 优化方法一
function Component(props) {
  const handleChange = useCallback(() => props.callback(props.id), [props.id]);
  return <AnotherComponent onChange={handleChange} />  
}

// 优化方法二
class Component extends React.Component {
  handleChange = () => {
   this.props.callback(this.props.id) 
  }
  render() {
    return <AnotherComponent onChange={this.handleChange} />
  }
}

使用React.Fragment避免添加额外的DOM

有些情况下,我们需要在组件中返回多个元素,例如下面的元素,但是在react规定组件中必须有一个父元素。

            <h1>Hello world!</h1>
            <h1>Hello there!</h1>
            <h1>Hello there again!</h1>

如果这样做会创建额外的不必要的div。这会导致整个应用程序内创建许多无用的元素:

function Component() {
        return (
            <div>
                <h1>Hello world!</h1>
                <h1>Hello there!</h1>
                <h1>Hello there again!</h1>
            </div>
        )
}

实际上页面上的元素越多,加载所需的时间就越多。为了减少不必要的加载时间,我们可以使React.Fragment来避免创建不必要的元素。 Fragments简写形式<></>

function Component() {
        return (
            <React.Fragment>
                <h1>Hello world!</h1>
                <h1>Hello there!</h1>
                <h1>Hello there again!</h1>
            </React.Fragment>
        )
}

一些思考?

useState

该使用单个 state 变量还是多个 state 变量?
比如

//第一种
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

//第二种:也可以这样把它们放在同一个obj里面,这样只需一个state
const [state, setState] = useState({
  width: 100,
  height: 100,
  left: 0,
  top: 0
});

不同: 1、如果使用单个 state 变量,每次更新 state 时需要合并之前的 state。因为 useState 返回的 setState 会替换原来的值。这一点和 Class 组件的 this.setState 不同。this.setState 会把更新的字段自动合并到 this.state 对象中。 2、使用多个 state 变量可以让 state 的粒度更细,更易于逻辑的拆分和组合 3、如果粒度过细,代码就会变得比较冗余。如果粒度过粗,代码的可复用性就会降低 总结:

  1. 将完全不相关的 state 拆分为多组 state。比如 size 和 position
  2. 如果某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state。比如 left 和 topwidth 和 height
const [position, setPosition] = useState({top: 0, left: 0});
const [size, setSize] = useState({width: 100, height: 100});

useCallBack&&useMemo

有时候我们会对创建函数开销进行评估。每次render 中创建函数可能会开销比较大,为了避免函数多次创建,使用了 useMemo 或者 useCallback。但是对于现代浏览器来说,创建函数的成本微乎其微。因此,我们没有必要使用 useMemo 或者 useCallback 去节省这部分性能开销。当然,如果是为了保证每次 render 时回调的引用相等,你可以放心使用 useMemo 或者 useCallback

useMemo

useMemo本身也有开销。useMemo 会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo 可能会影响程序的性能。

要想合理使用 useMemo,我们需要搞清楚 useMemo 适用的场景:

  • 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。
  • 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。
interface Props {
  page: number;
  type: string;
}

const App = ({page, type}: Props) => {
  const result = useMemo(() => {
    return getResult(page, type);
  }, [page, type]);

  return <child result={result}/>;
};

在上面的例子中,渲染 ExpensiveComponent 的开销很大。所以,当 resolvedValue 的引用发生变化时,作者不想重新渲染这个组件。因此,作者使用了 useMemo,避免每次 render 重新计算 resolvedValue,导致它的引用发生改变,从而使下游组件 re-render。

这个担忧是正确的,但是使用 useMemo 之前,我们应该先思考两个问题:

  1. 传递给 useMemo 的函数开销大不大?在上面的例子中,就是考虑 getResolvedValue 函数的开销大不大。JS 中大多数方法都是优化过的,比如 Array.mapArray.forEach 等。如果你执行的操作开销不大,那么就不需要记住返回值。否则,使用 useMemo 本身的开销就可能超过重新计算这个值的开销。因此,对于一些简单的 JS 运算来说,我们不需要使用 useMemo 来「记住」它的返回值。
  2. 当输入相同时,「记忆」值的引用是否会发生改变?在上面的例子中,就是当 page 和 type 相同时,resolvedValue 的引用是否会发生改变?这里我们就需要考虑 resolvedValue 的类型了。如果 resolvedValue 是一个对象,由于我们项目上使用「函数式编程」,每次函数调用都会产生一个新的引用。但是,如果 resolvedValue 是一个原始值(stringbooleannullundefinednumbersymbol),也就不存在「引用」的概念了,每次计算出来的这个值一定是相等的。也就是说,ExpensiveComponent 组件不会被重新渲染。

因此,如果 getResolvedValue 的开销不大,并且 resolvedValue 返回一个字符串之类的原始值,那我们完全可以去掉 useMemo, 因此,在使用 useMemo 之前,我们不妨先问自己几个问题:

  1. 要记住的函数开销很大吗?
  2. 返回的值是原始值吗?
  3. 记忆的值会被其他 Hook 或者子组件用到吗?

一、应该使用 useMemo 的场景

  1. 保持引用相等
  • 对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo
  • 自定义 Hook 中暴露出来的 object、array、函数等,都应该使用 useMemo 。以确保当值相同时,引用不发生变化。
  • 使用 Context 时,如果 Provider 的 value 中定义的值(第一层)发生了变化,即便用了 Pure Component 或者 React.memo,仍然会导致子组件 re-render。这种情况下,仍然建议使用 useMemo 保持引用的一致性。
  1. 成本很高的计算
  • 比如 cloneDeep 一个很大并且层级很深的数据

二、无需使用 useMemo 的场景

  1. 如果返回的值是原始值: stringbooleannullundefinednumbersymbol(不包括动态声明的 Symbol),一般不需要使用 useMemo
  2. 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件),且没有用到其他 Hook 的依赖数组中,一般不需要使用 useMemo

useMemo主要是用在子组件上而不是函数,函数用useCallback就可以了。如果子组件花销很大,useMemo可以避免由于父组件改变而导致子组件重新rerender。 useCallback 只是 useMemo 的一个语法糖,如果返回值是函数,都能用。当然,对于单个函数,用 useCallback 可能更方便。但是对于函数集合,用 useMemo 能更好地统一处理。

  1. useMemouseCallback不能盲目使用,因为他们都是基于闭包实现的,闭包会占用内存。
  2. 当依赖项频繁改动时,要考虑useMemo、useCallback是否划算,因为useCallback会频繁创建函数体。useMemo会频繁创建回调。

自定义hooks

可以实现业务逻辑复用 例:多个组件需根据窗口大小来设置某个元素宽度

export default function useWidth() {
  const [refWidth,setRefWidth]= useState(0)

  useEffect(() => {
    window.addEventListener('resize', handleResize); // 监听窗口大小改变
    const clientW = document.documentElement.clientWidth;
    handleResize(clientW)
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  handleResize = (clientW) => {
    setRefWidth(clientW)
  };

  return refWidth
}