MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 函数组件与 memo 的性能优化策略

2023-01-285.0k 阅读

React 函数组件基础

在 React 开发中,函数组件是构建用户界面的重要方式之一。函数组件本质上就是 JavaScript 函数,它接收输入的 props,并返回一个 React 元素来描述应该在屏幕上看到的内容。

import React from'react';

const MyComponent = (props) => {
  return <div>{props.message}</div>;
};

export default MyComponent;

上述代码定义了一个简单的函数组件 MyComponent,它接收一个 props 对象,从中取出 message 属性并渲染到 div 元素中。

函数组件相较于类组件,语法更为简洁,而且随着 React Hooks 的出现,函数组件能够实现类组件几乎所有的功能,甚至在某些方面更加灵活和强大。例如,使用 useState Hook 可以在函数组件中添加状态,使用 useEffect Hook 可以处理副作用操作。

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

const CounterComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default CounterComponent;

CounterComponent 中,useState 为组件添加了一个 count 状态以及更新它的 setCount 函数。useEffect 则在 count 变化时更新文档标题,模拟了类组件中 componentDidUpdate 的功能。

React 中的渲染机制

React 采用的是虚拟 DOM(Virtual DOM)机制来高效地更新实际 DOM。当组件的状态或 props 发生变化时,React 会创建一个新的虚拟 DOM 树,并与之前的虚拟 DOM 树进行对比(称为 diffing 算法)。通过对比找出最小的变化集,然后将这些变化应用到实际 DOM 上,从而实现高效的更新。

import React, { useState } from'react';

const ListComponent = () => {
  const [items, setItems] = useState([1, 2, 3]);

  const addItem = () => {
    setItems([...items, items.length + 1]);
  };

  return (
    <div>
      <ul>
        {items.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
      <button onClick={addItem}>Add Item</button>
    </div>
  );
};

export default ListComponent;

ListComponent 中,每次点击 “Add Item” 按钮,items 状态更新,React 会重新渲染组件。React 通过 diffing 算法,仅会更新新增的 <li> 元素,而不是重新渲染整个列表,这大大提高了渲染效率。

然而,在某些复杂场景下,如果处理不当,不必要的渲染仍可能发生,导致性能问题。例如,当父组件的状态变化时,即使子组件的 props 没有改变,子组件也会默认重新渲染。

import React, { useState } from'react';

const ParentComponent = () => {
  const [parentState, setParentState] = useState(0);

  const ChildComponent = () => {
    return <div>Child Component</div>;
  };

  return (
    <div>
      <button onClick={() => setParentState(parentState + 1)}>
        Update Parent State
      </button>
      <ChildComponent />
    </div>
  );
};

export default ParentComponent;

在上述代码中,ChildComponent 并没有依赖 parentState,但每次点击按钮更新 parentState 时,ChildComponent 也会重新渲染,这是不必要的性能开销。

React.memo 原理

React.memo 是 React 提供的一个高阶组件(HOC),用于对函数组件进行性能优化。它通过浅比较 props 来决定组件是否需要重新渲染。

当一个函数组件被 React.memo 包裹时,React 会在组件接收到新的 props 时,对新旧 props 进行浅比较。如果新旧 props 的引用相等,或者它们的所有一级属性引用都相等,React 认为 props 没有变化,组件不需要重新渲染,从而直接复用之前的渲染结果。

import React from'react';

const MyMemoizedComponent = React.memo((props) => {
  return <div>{props.value}</div>;
});

export default MyMemoizedComponent;

在这个例子中,MyMemoizedComponentReact.memo 包裹。如果父组件传递给它的 props.value 引用没有变化,那么即使父组件重新渲染,MyMemoizedComponent 也不会重新渲染。

浅比较的局限性

虽然 React.memo 的浅比较机制在大多数情况下能够有效减少不必要的渲染,但它也存在局限性。浅比较只比较对象或数组的引用,而不是它们的实际内容。

import React, { useState } from'react';

const MyMemoizedList = React.memo((props) => {
  return (
    <ul>
      {props.items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
});

const ParentForMemoLimit = () => {
  const [list, setList] = useState([1, 2, 3]);

  const updateList = () => {
    const newList = [...list];
    newList[0] = 4;
    setList(newList);
  };

  return (
    <div>
      <button onClick={updateList}>Update List</button>
      <MyMemoizedList items={list} />
    </div>
  );
};

export default ParentForMemoLimit;

在上述代码中,MyMemoizedListReact.memo 包裹。当点击 “Update List” 按钮时,虽然 list 数组的内容发生了变化,但由于使用了展开运算符创建的新数组引用与之前不同,React.memo 的浅比较认为 props.items 没有变化,MyMemoizedList 不会重新渲染,这就导致页面上的列表不会更新到最新状态。

性能优化策略实践

1. 合理使用 React.memo

在组件的 props 相对稳定,或者 props 的变化对组件渲染结果影响不大的情况下,使用 React.memo 可以显著提升性能。

import React, { useState } from'react';

const DisplayText = React.memo((props) => {
  return <p>{props.text}</p>;
});

const TextUpdater = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('Initial Text');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setText('New Text')}>Update Text</button>
      <DisplayText text={text} />
    </div>
  );
};

export default TextUpdater;

TextUpdater 组件中,DisplayText 组件只依赖 text prop。当点击 “Increment Count” 按钮时,count 变化但 text 不变,由于 DisplayTextReact.memo 包裹,它不会重新渲染,提高了性能。

2. 深度比较 props

为了解决 React.memo 浅比较的局限性,可以手动进行深度比较。虽然深度比较性能开销较大,但在某些关键场景下是必要的。

import React from'react';

const isEqual = (obj1, obj2) => {
  if (typeof obj1!== 'object' || typeof obj2!== 'object' || obj1 === null || obj2 === null) {
    return obj1 === obj2;
  }

  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);

  if (keys1.length!== keys2.length) {
    return false;
  }

  for (let key of keys1) {
    if (!isEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
};

const MyDeepMemoizedComponent = React.memo((props) => {
  return <div>{props.data.value}</div>;
}, (prevProps, nextProps) => {
  return isEqual(prevProps.data, nextProps.data);
});

export default MyDeepMemoizedComponent;

在上述代码中,MyDeepMemoizedComponent 通过自定义的 isEqual 函数进行深度比较。只有当 props.data 的内容真正发生变化时,组件才会重新渲染。

3. 拆分组件

将大型组件拆分成多个小组件,并且对每个小组件合理应用 React.memo,可以进一步优化性能。

import React, { useState } from'react';

const Item = React.memo((props) => {
  return <li>{props.value}</li>;
});

const List = React.memo((props) => {
  return (
    <ul>
      {props.items.map((item) => (
        <Item key={item} value={item} />
      ))}
    </ul>
  );
});

const ListManager = () => {
  const [list, setList] = useState([1, 2, 3]);

  const addItemToList = () => {
    setList([...list, list.length + 1]);
  };

  return (
    <div>
      <button onClick={addItemToList}>Add Item</button>
      <List items={list} />
    </div>
  );
};

export default ListManager;

ListManager 组件中,List 组件和 Item 组件都被 React.memo 包裹。当添加新项时,只有新增的 Item 组件会重新渲染,List 组件和其他 Item 组件由于 React.memo 的作用不会重新渲染,提升了整体性能。

4. 使用 useMemo 和 useCallback

useMemouseCallback 也是 React 中重要的性能优化工具,它们与 React.memo 配合使用能发挥更大效果。

useMemo 用于缓存计算结果,只有当依赖项变化时才重新计算。

import React, { useState, useMemo } from'react';

const ExpensiveCalculation = (props) => {
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += i;
  }
  return result;
};

const MemoizedCalculation = () => {
  const [count, setCount] = useState(0);

  const memoizedResult = useMemo(() => ExpensiveCalculation(), []);

  return (
    <div>
      <p>Memoized Result: {memoizedResult}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
};

export default MemoizedCalculation;

MemoizedCalculation 组件中,ExpensiveCalculation 函数只在组件挂载时执行一次,因为 useMemo 的依赖项数组为空。即使点击按钮更新 countmemoizedResult 也不会重新计算,避免了不必要的性能开销。

useCallback 用于缓存函数引用,只有当依赖项变化时才重新创建函数。这在将函数作为 props 传递给子组件时非常有用,特别是子组件被 React.memo 包裹的情况。

import React, { useState, useCallback } from'react';

const Child = React.memo((props) => {
  return <button onClick={props.onClick}>Click Me</button>;
});

const ParentForCallback = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <Child onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
};

export default ParentForCallback;

ParentForCallback 组件中,handleClick 函数通过 useCallback 进行了缓存。由于 Child 组件被 React.memo 包裹,只有当 handleClick 函数引用变化时,Child 组件才会重新渲染。而 useCallback 确保只有 count 变化时 handleClick 函数引用才会变化,避免了 Child 组件不必要的重新渲染。

性能优化的注意事项

1. 避免过度优化

虽然性能优化很重要,但过度优化可能导致代码变得复杂且难以维护。在应用性能优化策略之前,应该使用性能分析工具(如 React DevTools 的 Profiler 标签)来确定真正存在性能问题的组件和操作。

例如,在一个小型项目中,对每个组件都进行深度比较或复杂的缓存操作可能会增加代码复杂度,而对整体性能提升却不明显。

2. 注意依赖项管理

无论是 useMemouseCallback 还是 React.memo,正确管理依赖项至关重要。错误地设置依赖项可能导致组件无法及时更新或进行不必要的重新渲染。

对于 useMemouseCallback,如果依赖项数组设置不正确,可能会导致缓存结果不更新或者函数引用频繁变化。对于 React.memo,如果 props 中包含复杂对象且未进行正确处理(如深度比较),可能会出现组件不更新的问题。

3. 结合业务场景

性能优化策略应该结合具体的业务场景来实施。不同的业务场景对性能的要求和优化方向可能不同。

例如,在一个实时数据更新频繁的监控系统中,可能需要更注重数据变化时的高效渲染,而在一个相对静态的展示型页面中,可能更关注首次加载性能。

总结性能优化的整体流程

  1. 性能分析:使用 React DevTools 的 Profiler 等工具,找出应用中存在性能问题的组件。观察组件的渲染次数、渲染时间等指标,确定性能瓶颈。
  2. 评估优化点:分析性能瓶颈产生的原因,判断是由于不必要的渲染、复杂计算还是其他因素导致。例如,如果某个组件频繁重新渲染,需要检查其 props 和状态变化情况。
  3. 选择优化策略:根据分析结果选择合适的优化策略。如果是 props 变化导致的不必要渲染,可以考虑使用 React.memo;如果存在复杂计算,可以使用 useMemo 进行缓存。
  4. 实施优化:在代码中应用选定的优化策略,如包裹 React.memo、添加 useMemouseCallback 等。同时,确保依赖项设置正确,避免引入新的问题。
  5. 再次分析与验证:优化后再次使用性能分析工具,验证性能是否得到提升,是否引入了新的性能问题。如果性能没有明显改善,需要重新评估优化策略并进行调整。

通过以上流程,可以在 React 应用开发中有效地利用函数组件和 React.memo 等工具进行性能优化,提升用户体验。同时,持续关注性能优化,随着业务的发展和应用的变化及时调整优化策略,确保应用始终保持良好的性能表现。在实际项目中,还需要结合其他性能优化技术,如代码拆分、懒加载等,全方位提升 React 应用的性能。