介绍

我在 2021 年作为一名应届毕业生加入了一家网络开发公司,担任前端工程师,并将在 2022 年进入我的第二年。

在实践中,我主要使用 React x TypeScript 进行前端开发。

这一次,我将总结我在该领域所经历的 React 应用程序的性能优化。

本文的目标读者

  • React 初学者到中级
  • 想了解 React 性能优化的人

本文的目标

  • 了解 React 渲染的工作原理
  • 了解如何优化 React 性能
  • 了解React.memouseCallbackuseMemo

承诺

  • 使用React.memouseCallbackuseMemo的费用详解
  • 不要用数字来衡量性能

关于以上两点,我会在相关地方附上一篇参考文章。

关于 React 渲染

为了优化 React 的性能,我将解释 React 的渲染机制。

首先,什么是渲染?React 文档测试版解释如下。 (使用 DeepL 翻译)

“渲染”是 React 调用你的组件的时候。

  • 对于第一次渲染,React 调用根组件。
  • 在后续渲染中,React 将调用触发渲染的状态更新的函数组件。

简而言之,让 React 调用你的函数组件渲染我正在调用它。

渲染的工作原理React 文档测试版描述如下。

  1. 触发渲染(将客户订单带到厨房)
  2. 组件渲染(在厨房准备订单)
  3. 提交到 DOM(将订单放入表中)

    我将上面的渲染机制稍微分解并解释一下。

    1.触发渲染

    有两个触发渲染的事件:

    • 第一次渲染
    • 在屏幕刷新时重新渲染

    重新渲染通过使用state 的更新函数setState 更新状态并更新组件的状态来安排下一次渲染。 (获取差异并更新)

    2. React 渲染组件

    渲染开始时,它会调用 React 组件来确定要在屏幕上显示什么。此时,不进行对DOM的反射处理。

    • 第一次渲染:React 调用根组件
    • 后续渲染:调用函数组件进行状态更新由 1 触发

    如果更新的组件有子组件,React 接下来会渲染这些子组件。这个过程是递归的。

    Reactパフォーマンス最適化まとめ

    这种情况下,当父组件(Parent)更新状态时,子组件A、子组件B、子组件C、子组件D也会渲染。

    代码如下所示:

    父.tsx
    // 状態更新が発火する親コンポーネント
    export const Parent = () => {
      const [count, setCount] = useState<number>(0);
      const onClick = () => {
        setCount(count + 1);
      };
      return (
        <>
          <button onClick={onClick}>+1</button>
          <ChildA />
          <ChildB />
        </>
      );
    };
    
    ChildA.tsx
    export const ChildA = () => {
      return (
        <>
          <p>子コンポーネントA</p>
          <ChildC />
        </>
      );
    };
    
    ChildB.tsx
    export const ChildB = () => {
      return (
        <>
          <p>子コンポーネントA</p>
          <ChildD />
        </>
      );
    };
    

    后面我会详细解释,但是当Parent组件的state更新时,虽然只有Parent使用(ChildA到ChildD不依赖),但是ChildA到ChildD也会被渲染掉。

    您可以通过消除这种不必要的渲染来优化 React 的性能。

    3. React 提交对 DOM 的更改

    组件渲染后,React 将反映在 DOM 中,更新后的浏览器将重绘屏幕。

    综上所述,React 应用程序的屏幕更新是通过以下 3 个步骤完成的

    • 触发渲染
    • 渲染组件
    • 提交对 DOM 的更改

    关于避免状态更新

    React 官方文档比,

    如果您使用与当前值相同的值进行更新,React 将退出而不渲染任何子级或执行副作用。

    如果setState 与当前state 具有相同的值,它将避免渲染。

    让我们看一个具体的代码示例。

    export const Parent = () => {
      const [count, setCount] = useState<number>(0);
      const onClick = () => {
        setCount(1);
      };
      console.log("レンダリング");
      return (
        <>
          <button onClick={onClick}>+1</button>
          <p>count: {count}</p>
        </>
      );
    };
    

    在上述组件中,渲染触发是因为在初始渲染和按钮单击时计数更新为 1。

    即使之后点击按钮,当前state为1,setState的参数值也为1,所以当前位置和更新值相同,所以不会进行渲染。 (避免)

    反应性能优化

    在本文中,我们将介绍以下三种优化 React 性能的方法。

    • React.memo
    • 使用回调
    • 使用备忘录

    让我们仔细看看每一个。

    反应备忘录

    React.memo 是官方文件解释如下。

    如果一个组件在给定相同的 props 时呈现相同的结果,您可以将其包装在 React.memo 中以记住结果并提高性能。换句话说,React 将跳过渲染组件并重用最后的渲染结果。

    稍微分解一下,通过将React.memo包裹在一个子组件中,如果子组件从父组件接收到的props的值没有改变,则子组件可以跳过渲染。

    这意味着您可以在不触发不必要的渲染的情况下提高性能。

    [React.memo 的语法]

    React.memo(メモ化したいコンポーネント);
    

    我们来看一个组件的例子,它管理count并更新父子中不同count的状态。

    Reactパフォーマンス最適化まとめ
    [不使用 React.memo 时]

    父.tsx
    export const Parent = () => {
      const [parentCount, setParentCount] = useState<number>(0);
      const [childCount, setChildCount] = useState<number>(0);
    
      const addParentCount = () => {
        setParentCount(parentCount + 1);
      };
      const addChildCount = () => {
        setChildCount(childCount + 1);
      };
      return (
        <>
          <button onClick={addParentCount}>親のカウントを+1</button>
          <p>親のカウント: {parentCount}</p>
          <button onClick={addChildCount}>子のカウントを+1</button>
          <Child count={childCount} />
        </>
      );
    };
    
    孩子.tsx
    type ChildProps = {
      count: number;
    };
    
    export const Child: React.FC<ChildProps> = ({ count }) => {
      return (
        <>
          <p>子のcount:{count}</p>
        </>
      );
    };
    

    如果单击按钮增加子组件的数量,您可以看到父子组件都呈现如下所示。

    Reactパフォーマンス最適化まとめ

    同样,点击按钮增加父组件的计数。您还可以确认父母和孩子都被渲染。

    Reactパフォーマンス最適化まとめ

    这里,当父组件的parentCount更新时,即使依赖于子组件的值没有更新,渲染进程也在运行。

    React.memo 包裹子组件以避免这种不必要的渲染。

    孩子.tsx
    export const Child: React.FC<ChildProps> = ({ count }) => {
      console.log("子供コンポーネントのレンダリング");
      return (
        <>
          <p>子のカウント:{count}</p>
        </>
      );
    };
    
    export const ChildMemo = React.memo(Child);
    

    如果再次点击增加父组件计数的按钮,可以看到只渲染父组件,没有更新值的ChildMemo组件没有渲染。

    Reactパフォーマンス最適化まとめ

    通过如上所述使用React.memo,您可以通过在props 传递的值没有变化时避免不必要的渲染来优化性能。

    使用回调

    useCallback官方文件解释如下。

    传递一个内联回调数组和它们所依赖的值。 useCallback 返回该回调的记忆版本,并且该函数仅在任何依赖数组元素发生更改时才更改。
    这对于将回调传递给经过优化以查找引用相等性的组件(例如,使用 shouldComponentUpdate )以避免不必要的渲染非常有用。

    稍微分解一下,只有当useCallback 的第二个参数指定的依赖数组的任何元素发生变化时,它才会重新计算记忆值。

    换句话说,如果依赖数组的元素没有改变,就可以避免不必要的渲染。

    [useCallback 的语法]

    useCallback(コールバック関数, 依存配列);
    

    让我们仔细看看代码。

    除了子组件中的count,还可以从props 的父组件接收更新(+1)count 状态的函数(onClickChild)。

    孩子.tsx
    type ChildProps = {
      count: number;
      onClickChild: () => void;
    };
    
    export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
      console.log("子供コンポーネントのレンダリング");
      return (
        <>
          <button onClick={onClickChild}>子のカウントを+1</button>
    
          <p>子のカウント:{count}</p>
        </>
      );
    };
    export const ChildMemo = React.memo(Child);
    
    父.tsx
    export const Parent = () => {
      const [parentCount, setParentCount] = useState<number>(0);
      const [childCount, setChildCount] = useState<number>(0);
    
      const addParentCount = () => {
        setParentCount(parentCount + 1);
      };
      const addChildCount = () => {
        setChildCount(childCount + 1);
      };
      console.log("親コンポーネントのレンダリング");
    
      return (
        <>
          <button onClick={addParentCount}>親のカウントを+1</button>
          <p>親のカウント: {parentCount}</p>
          <ChildMemo count={childCount} onClickChild={addChildCount} />
        </>
      );
    };
    

    Reactパフォーマンス最適化まとめ

    如果在此状态下单击按钮增加子计数(onClickChild),您可以看到父组件和子组件都从控制台渲染。

    Reactパフォーマンス最適化まとめ

    然后单击不依赖子组件的按钮更新父count

    Reactパフォーマンス最適化まとめ

    然后,您可以看到子组件已被渲染,即使该组件之前已被记忆。

    原因是在props接收到的onClickChild在每次父组件重新渲染时都会重新计算,结果在onClickChild被传递给React的时机,一个新的函数被传递给props side 知道并且即使在子组件中也正在运行渲染。

    因此,即使使用React.memo 记忆组件主体,渲染也会运行。

    为了避免这种不必要的渲染,props 中传递的函数 (onClickChild) 被包裹在 useCallback 中并被记忆。

    父.tsx
    export const Parent = () => {
      const [parentCount, setParentCount] = useState<number>(0);
      const [childCount, setChildCount] = useState<number>(0);
    
      const addParentCount = () => {
        setParentCount(parentCount + 1);
      };
      // メモ化
      const addChildCount = useCallback(() => {
        setChildCount(childCount + 1);
      }, []);
      console.log("親コンポーネントのレンダリング");
    
      return (
        <>
          <button onClick={addParentCount}>親のカウントを+1</button>
          <p>親のカウント: {parentCount}</p>
          <ChildMemo count={childCount} onClickChild={addChildCount} />
        </>
      );
    };
    

    同样,按下增加父级计数的按钮可确认避免了子级的渲染。

    Reactパフォーマンス最適化まとめ

    但是,我遇到了一个问题,即单击按钮以增加子组件计数不会从 1 增加它。

    Reactパフォーマンス最適化まとめ

    这是因为在前面定义的useCallback 的第二个参数中传递了一个空数组,所以用useCallback 包裹的函数在第一次渲染期间保留在React 中。

    在代码方面,setChildCount 的值保持为 1 如下所示,因此该值不会从 1 更新。

      const [childCount, setChildCount] = useState<number>(0);
    
      const addChildCount = useCallback(() => {
        // setChildCount(childCount + 1);
        // setChildCount(0 + 1);
           setChildCount(1);
      }, []);
    

    为了防止这种情况发生,通过插入依赖于useCallback的第二个参数的依赖数组的状态(这次是childCount),在childCount的值更新时执行useCallback中定义的函数。 .

      const [childCount, setChildCount] = useState<number>(0);
    
      const addChildCount = useCallback(() => {
        setChildCount(childCount + 1);
        // 1回目ボタンがクリックされた時
        // setChildCount(1);
        // 2回目ボタンがクリックされた時
        // setChildCount(1+1);
        // 3回目ボタンがクリックされた時
        // setChildCount(2+1);
      }, [childCount]);
    

    通过如上所述使用useCallback,即使函数作为props 传递,也可以避免不必要的渲染。

    使用备忘录

    使用备忘录在官方文档中是这样描述的:

    如果任何依赖数组元素发生变化,useMemo 只会重新计算记忆值。这种优化避免了在每次渲染时执行昂贵的计算。

    React.memomemoized 组件和useCallbackmemoized 回调函数,但是useMemomemoize 计算值(数字和渲染结果),避免不必要的渲染。

    [useMemo 的语法]

    useMemo(() => メモ化したい計算ロジック, 依存配列);
    

    具体来说,之前使用React.memo 转换为memo 的组件中的JSX 将使用useMemo 进行记忆。

    [之前用 React.memo 记忆的子组件]

    type ChildProps = {
      count: number;
      onClickChild: () => void;
    };
    
    export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
      console.log("子供コンポーネントのレンダリング");
      return (
        <>
          <button onClick={onClickChild}>子のカウントを+1</button>
          <p>子のカウント:{count}</p>
        </>
      );
    };
    
    export const ChildMemo = React.memo(Child);
    

    [用 useMemo 记忆 JSX]

    type ChildProps = {
      count: number;
      onClickChild: () => void;
    };
    
    export const Child: React.FC<ChildProps> = ({ count, onClickChild }) => {
      console.log("子供コンポーネントのレンダリング");
      return useMemo(() => {
        console.log("メモ化した値");
        return (
          <>
            <button onClick={onClickChild}>子のカウントを+1</button>
            <p>子のカウント:{count}</p>
          </>
        );
      }, [count, onClickChild]);
    };
    

    我将console 放在useMemo 的内部和外部,以便于确认记忆。

    我会尝试增加父组件的数量。

    Reactパフォーマンス最適化まとめ

    然后你可以看到没有被记忆的console.log("子供コンポーネントのレンダリング");(在useMemo之外)正在运行。

    在前面介绍的 React.memo 中,整个组件都是 memoized 的,所以在执行 parent count 的时候子组件没有渲染。

    Reactパフォーマンス最適化まとめ

    接下来,点击按钮增加子组件的count

    Reactパフォーマンス最適化まとめ

    然后可以看到useMemo处记忆的JSX值也被执行了。

    在最后

    怎么样。在本文中,我们总结了优化 React 性能的基本方法。

    我们希望您使用这里介绍的方法来开发性能更好的产品。

    下一次,我想介绍如下数据通信中的性能优化。

    • 使用 useQuery 和 useSWR 优化获取

    我还有其他关于 React 的文章,所以如果你能阅读它们,我会很高兴。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308628151.html

相关文章:

  • 2021-04-30
  • 2021-10-05
  • 2021-11-08
  • 2021-11-23
  • 2021-11-17
  • 2021-11-02
猜你喜欢
  • 2021-08-27
  • 2021-12-02
  • 2021-10-03
  • 2021-10-04
  • 2021-11-12
  • 2021-08-21
  • 2021-12-03
  • 2021-08-30
相关资源
相似解决方案