React性能优化方案

React的性能优化通常涉及不必要的渲染、优化组件结构和提高应用程序的响应速度。

而大家都知道React分为函数组件和类组件,这里主要介绍函数组件,当然也会提及少量的类组件。

避免不必要的渲染

1.Memo

Memo是一个高阶组件,类似于PureComponent(后面补充),它接受一个函数组件作为参数,返回一个新的函数组件

新的函数组件会对传入的props进行浅比较,如果props没有发生变化,则不会重新渲染组件,反之重新渲染组件。

基础示例

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

// 未使用memo的子组件
const ChildWithoutMemo = ({ name, count }: { name: string; count: number }) => {
  console.log('ChildWithoutMemo渲染了');
  return <div>{name}: {count}</div>;
};

// 使用memo的子组件
const ChildWithMemo = memo(({ name, count }: { name: string; count: number }) => {
  console.log('ChildWithMemo渲染了');
  return <div>{name}: {count}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [unrelatedState, setUnrelatedState] = useState(0);

  return (
    <div>
      <ChildWithoutMemo name="未优化" count={count} />
      <ChildWithMemo name="已优化" count={count} />

      <button onClick={() => setCount(count + 1)}>增加count</button>
      {/* 点击此按钮时,ChildWithoutMemo会重新渲染,但ChildWithMemo不会 */}
      <button onClick={() => setUnrelatedState(unrelatedState + 1)}>
        更新无关状态
      </button>
    </div>
  );
}

在这个例子中,当点击"更新无关状态"按钮时,父组件重新渲染,但由于传给ChildWithMemo的props没有变化,它不会重新渲染。而ChildWithoutMemo每次都会重新渲染。

memo的源码其实就是返回一个具有类似于PureComponent的浅比较的函数组件。

如果在子组件的props中包含有复杂的数据结构,比如对象、数组等,那么memo的浅比较可能会失效,导致组件重新渲染。这时我们可以习惯性的返回一个新的对象或者数组来避免浅比较问题

补充

memo还接受第二个参数,用来自定义比较的逻辑。

第二个参数是一个函数,包含两个参数:oldProps和newProps,返回的是一个bool值。如果函数返回true,则表示props没有发生变化,组件不会重新渲染,反之重新渲染组件。

2.useCallback

useCallback是一个hooks,用于缓存函数来优化函数组件的性能,避免函数在每次渲染时都重新创建。

useCallback接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useCallback会返回一个新的函数,否则返回缓存的函数。

memo与useCallback联合使用

仅仅使用memo时,如果传入的props是一个函数,那么每次渲染时都会重新创建这个函数,导致组件重新渲染。这时我们可以使用useCallback来缓存这个函数,避免每次渲染时都重新创建函数。

在实际开发中,我们通常会将memo和useCallback结合使用,以减少不必要的组件渲染和函数创建,从而提高性能

示例:memo + useCallback 组合优化

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

// 子组件:接收一个函数作为prop
const ExpensiveList = memo(({
  items,
  onItemClick
}: {
  items: string[];
  onItemClick: (item: string) => void
}) => {
  console.log('ExpensiveList渲染了');
  return (
    <ul>
      {items.map(item => (
        <li key={item} onClick={() => onItemClick(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
});

function TodoApp() {
  const [items] = useState(['任务1', '任务2', '任务3']);
  const [count, setCount] = useState(0);

  // 错误做法:每次渲染都创建新函数,导致memo失效
  // const handleClick = (item: string) => {
  //   console.log('点击了:', item);
  // };

  // 正确做法:使用useCallback缓存函数
  const handleClick = useCallback((item: string) => {
    console.log('点击了:', item);
  }, []); // 空依赖数组,函数永远不会改变

  return (
    <div>
      <ExpensiveList items={items} onItemClick={handleClick} />

      {/* 更新count不会导致ExpensiveList重新渲染 */}
      <button onClick={() => setCount(count + 1)}>
        计数: {count}
      </button>
    </div>
  );
}

在这个例子中:

  • 如果不使用useCallback,每次TodoApp渲染时都会创建新的handleClick函数
  • 即使使用了memo,由于函数引用变化,ExpensiveList仍会重新渲染
  • 使用useCallback后,函数引用保持不变,memo才能真正发挥作用

3.useMemo

useMemo是一个hook,用于缓存计算结果,避免在每次渲染时都重新计算(类似于Vue中的Computed)。

useMemo接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useMemo会返回一个新的计算结果,否则返回缓存的计算结果。

(后面补充例子)

4.useRef

useRef是一个hook,用于创建一个可变的ref对象,该对象在组件的整个生命周期内保持不变。

useRef可以用于缓存数据,避免在每次渲染时都重新创建数据。同时,useRef还可以用于获取DOM元素

(后面补充例子)

优化组件结构

1.拆分组件

将一个大的组件拆分成多个小的组件,每个组件只负责一个功能,这样可以提高组件的可维护性和可复用性。

2.使用函数组件

函数组件相对于类组件来说,更加简洁和易于理解,同时函数组件的性能也更好。

3.使用Hooks

Hooks是React 16.8引入的新特性,它允许我们在函数组件中使用状态和其他React特性,同时Hooks的性能也更好。

4.使用Context

Context是React提供的一种跨组件通信的方式,它可以避免通过props一层层传递数据,从而提高组件的性能。

(后续中...)