React 组件性能优化

390 阅读3分钟

前言

无论是类组件还是函数式组件,React 性能优化的主要方向有以下两个:

  1. 减少组件的非必要的重新渲染

  2. 减少组件内部的重复计算

本文将介绍一些在平时开发中用的比较多的 React.memo、useMemo 和 useCallback......

React.memo

React.memo 是一个 高阶组件

React.memo 包裹的组件在渲染前,会对新旧的 props 进行浅比较:

  • 如果新旧 props 浅比较相等,则不进行重新渲染(使用缓存的组件)。

  • 如果新旧 props 浅比较不相等,则进行重新渲染(重新渲染的组件)。

在没有任何优化的情况下,React 中某一组件重新渲染,会导致其全部的子组件重新渲染。即通过 React.memo 的包裹,在其父组件重新渲染时,可以避免这个组件的非必要重新渲染。

注意:【渲染】指的是 React 执行函数组件并生成或更新虚拟 DOM 树(Fiber 树)的过程。在渲染真实 DOM (Commit 阶段)前还有 DOM Diff 的过程,会比对虚拟 DOM 之间的差异,再去渲染变化的 DOM。

useMemo

const memolized = useMemo(fn, deps);

useMemo 把计算函数 fn 和依赖项数组 deps 作为参数,useMemo 会执行 fn 并返回一个缓存值 memolized,它仅会在某个依赖项改变时才重新计算 memolized。这种优化有助于避免组件在每次渲染时都进行高开销的计算。

示例:

// 缓存计算 list 的和
const memoSum = useMemo(() => {
  console.log("useMemo 计算");
  return list.reduce((previous, current) => previous + current);
}, [list]);

在函数组件内部,一些基于 State 的衍生值和一些复杂的计算可以通过 useMemo 进行性能优化。

useCallback

const memolizedCallback = useCallback(fn, deps);

useCallback 把回调函数 fn 和依赖项数组 deps 作为参数,并返回一个缓存的回调函数 memolizedCallback (本质上是一个引用),它仅会在某个依赖项改变时才重新生成 memolizedCallback。当你把 memolizedCallback 作为参数传递给子组件(被 React.memo 包裹)时,它可以避免非必要的子组件重新渲染。

正确使用场景:

  1. 函数组件内部定义的函数需要作为其他 Hooks 的依赖。

  2. 函数组件内部定义的函数需要传递给其子组件,并且子组件由 React.memo 包裹。

场景 1:useCallback 主要是为了避免当组件重新渲染时,函数引用变动所导致其它 Hooks 的重新执行,更为甚者可能造成组件的无限渲染:

import React, { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(1);
  const add = () => {
    setCount((count) => count + 1);
  };

  useEffect(() => {
    add();
  }, [add]);

  return <div className="App">count: {count}</div>;
}

export default App;

上例中,useEffect 会执行 add 函数从而触发组件的重新渲染,函数的重新渲染会重新生成 add 的引用,从而触发 useEffect 的重新执行,然后再执行 add 函数触发组件的重新渲染...,从而导致无限循环。

为了避免上述的情况,我们给 add 函数套一层 useCallback 避免函数引用的变动,就可以解决无限循环的问题:

import React, { useCallback, useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(1);
  // 用 useCallback 包裹 add ,只会在组件第一次渲染生成函数引用,之后组件重新渲染时,add 会复用第一次生成的引用。
  const add = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  useEffect(() => {
    add();
  }, [add]);

  return <div className="App">count: {count}</div>;
}

export default App;

场景 2:useCallback 是为了避免由于回调函数引用变动,所导致的子组件非必要重新渲染。(这个子组件有两个前提:首先是接收回调函数作为 props,其次是被 React.memo 所包裹。

总结

  • 通过 React.memo 包裹组件,可以避免组件的非必要重新渲染。

  • 通过 useMemo,可以避免组件更新时所引发的重复计算。

  • 通过 useCallback,可以避免由于函数引用变动所导致的组件重复渲染。