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

React State 的性能优化策略

2021-10-022.5k 阅读

React State 简介

在 React 应用中,state 是一个至关重要的概念。它代表了组件内部的可变数据,这些数据的变化会触发组件的重新渲染,进而更新用户界面。例如,一个简单的计数器组件:

import React, { useState } from 'react';

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

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

export default Counter;

在这个例子中,count 就是 Counter 组件的 state。当点击按钮时,count 的值发生变化,从而导致组件重新渲染,页面上显示的数字也随之更新。

React State 性能问题产生的原因

  1. 不必要的重新渲染:在 React 中,只要组件的 stateprops 发生变化,组件就会重新渲染。然而,很多时候这种重新渲染可能是不必要的。比如,一个包含多个子组件的父组件,当父组件的 state 中某个与子组件无关的属性发生变化时,子组件也会被重新渲染。
import React, { useState } from'react';

const ChildComponent = ({ value }) => {
  return <p>{value}</p>;
};

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  return (
    <div>
      <ChildComponent value={name} />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
};

export default ParentComponent;

在这个例子中,ChildComponent 只依赖于 name 属性,但是当点击按钮增加 count 时,ParentComponent 会重新渲染,进而导致 ChildComponent 也重新渲染,尽管 ChildComponent 所依赖的数据并没有变化。 2. 频繁的 state 更新:如果在短时间内频繁地更新 state,会导致大量的重新渲染,从而影响性能。例如,在一个循环中多次调用 setState

import React, { useState } from'react';

const FrequentUpdateComponent = () => {
  const [list, setList] = useState([]);

  const addItems = () => {
    for (let i = 0; i < 1000; i++) {
      setList([...list, i]);
    }
  };

  return (
    <div>
      <button onClick={addItems}>Add Items</button>
      <ul>
        {list.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default FrequentUpdateComponent;

在这个例子中,每次调用 addItems 函数,setList 会被调用 1000 次,导致 1000 次重新渲染,这会严重影响性能。

性能优化策略

1. 使用 React.memouseMemouseCallback

  • React.memoReact.memo 是一个高阶组件,它可以用于 memoize 函数式组件。它会对组件的 props 进行浅比较,如果 props 没有变化,组件将不会重新渲染。
import React from'react';

const MemoizedChild = React.memo(({ value }) => {
  return <p>{value}</p>;
});

const ParentComponentWithMemo = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  return (
    <div>
      <MemoizedChild value={name} />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
};

export default ParentComponentWithMemo;

在这个改进后的例子中,MemoizedChild 组件只会在 name 属性变化时重新渲染,而当 count 变化时不会重新渲染。

  • useMemouseMemo 用于 memoize 一个值。它接收一个函数和一个依赖数组作为参数,只有当依赖数组中的值发生变化时,才会重新计算 memoized 值。
import React, { useState, useMemo } from'react';

const ExpensiveCalculationComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  const expensiveValue = useMemo(() => {
    // 模拟一个复杂的计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i;
    }
    return result;
  }, [name]);

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

export default ExpensiveCalculationComponent;

在这个例子中,expensiveValue 只有在 name 变化时才会重新计算,而当 count 变化时不会重新计算,避免了不必要的复杂计算。

  • useCallbackuseCallback 用于 memoize 一个函数。它接收一个回调函数和一个依赖数组作为参数,只有当依赖数组中的值发生变化时,才会重新生成新的回调函数。这在将回调函数传递给子组件时非常有用,可以避免子组件因为父组件传递的回调函数变化而不必要的重新渲染。
import React, { useState, useCallback } from'react';

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

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

  return (
    <div>
      <Child onClick={handleClick} />
    </div>
  );
};

export default ParentWithCallback;

在这个例子中,handleClick 函数只有在 count 变化时才会重新生成,这样 Child 组件只有在 handleClick 函数真的变化时才会重新渲染。

2. 合并 state 更新

正如前面提到的,频繁的 state 更新会导致性能问题。我们可以通过合并 state 更新来减少重新渲染的次数。在 React 类组件中,setState 会自动合并 state。在函数组件中,我们可以手动合并 state 更新。

import React, { useState } from'react';

const MergeStateComponent = () => {
  const [user, setUser] = useState({
    name: 'John',
    age: 30
  });

  const updateUser = () => {
    setUser(prevUser => ({
     ...prevUser,
      age: prevUser.age + 1,
      name: 'Jane'
    }));
  };

  return (
    <div>
      <p>Name: {user.name}, Age: {user.age}</p>
      <button onClick={updateUser}>Update User</button>
    </div>
  );
};

export default MergeStateComponent;

在这个例子中,updateUser 函数通过展开运算符合并了 state 的更新,这样只触发了一次重新渲染,而不是多次分别更新 nameage 导致多次重新渲染。

3. 合理使用 shouldComponentUpdate(类组件)或 useEffect 依赖数组(函数组件)

  • 类组件中的 shouldComponentUpdate:在 React 类组件中,shouldComponentUpdate 方法允许我们控制组件是否应该重新渲染。它接收 nextPropsnextState 作为参数,我们可以在这个方法中根据新旧 propsstate 的比较来决定是否返回 truefalse。如果返回 false,组件将不会重新渲染。
import React, { Component } from'react';

class CustomComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 只有当 name 属性变化时才重新渲染
    return this.props.name!== nextProps.name;
  }

  render() {
    return <p>{this.props.name}</p>;
  }
}

export default CustomComponent;
  • 函数组件中的 useEffect 依赖数组:在函数组件中,useEffect 的依赖数组可以控制副作用函数的执行时机。同样,我们可以利用这个机制来避免不必要的重新渲染。例如,如果一个副作用函数依赖于某个 state,我们可以将这个 state 放入依赖数组中,只有当这个 state 变化时,副作用函数才会执行。
import React, { useState, useEffect } from'react';

const EffectComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  useEffect(() => {
    // 只有当 name 变化时才执行这个副作用
    console.log('Name has changed:', name);
  }, [name]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setName('Jane')}>Change Name</button>
    </div>
  );
};

export default EffectComponent;

在这个例子中,useEffect 的副作用函数只有在 name 变化时才会执行,而当 count 变化时不会执行,避免了不必要的副作用执行。

4. 使用 Immutable Data

Immutable data 即不可变数据,在 React 中使用 immutable data 可以更高效地检测数据变化,从而优化性能。当数据是不可变的时,我们可以通过简单的引用比较来判断数据是否发生变化,而不需要进行深度比较。

import React, { useState } from'react';
import Immutable from 'immutable';

const ImmutableComponent = () => {
  const [list, setList] = useState(Immutable.List([1, 2, 3]));

  const addItem = () => {
    setList(list.push(4));
  };

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

export default ImmutableComponent;

在这个例子中,使用 immutable.js 库创建了一个不可变的列表。每次更新列表时,push 方法会返回一个新的不可变列表,通过引用比较就可以知道列表是否发生了变化,而不需要深度遍历列表来检测变化,提高了性能。

5. 虚拟滚动(Virtual Scrolling)

当处理大量数据的列表时,渲染所有数据会导致性能问题。虚拟滚动是一种优化技术,它只渲染当前视口内可见的项目,而不是渲染整个列表。在 React 中,有一些库可以帮助我们实现虚拟滚动,比如 react - virtualizedreact - window

import React from'react';
import { List } from'react - virtualized';

const data = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);

const rowRenderer = ({ index, key, style }) => {
  return (
    <div key={key} style={style}>
      {data[index]}
    </div>
  );
};

const VirtualScrollComponent = () => {
  return (
    <List
      height={400}
      rowCount={data.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
      width={300}
    />
  );
};

export default VirtualScrollComponent;

在这个例子中,使用 react - virtualizedList 组件实现了虚拟滚动。List 组件只会渲染当前视口内可见的项目,大大提高了性能,即使数据量很大也不会影响页面的流畅性。

6. 避免在 render 方法中执行复杂计算

render 方法是 React 组件渲染的核心部分,每次组件重新渲染时都会执行 render 方法。如果在 render 方法中执行复杂计算,会导致每次重新渲染都要重复这些计算,严重影响性能。

// 反例
import React, { useState } from'react';

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

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

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

// 改进后的例子
import React, { useState, useMemo } from'react';

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

  const expensiveValue = useMemo(() => {
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += i;
    }
    return result;
  }, []);

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

在反例中,每次 render 时都会执行 expensiveCalculation 函数,而在改进后的例子中,使用 useMemo 进行 memoize,只有在依赖数组中的值变化时(这里为空数组,即只在组件首次渲染时)才会执行复杂计算,提高了性能。

7. 优化初始 state

在组件初始化时,合理设置初始 state 也可以提升性能。避免在初始 state 中包含不必要的数据,因为这些数据可能会导致不必要的重新渲染。

// 反例
import React, { useState } from'react';

const BadInitialStateComponent = () => {
  const [user, setUser] = useState({
    name: 'John',
    age: 30,
    // 这个属性可能在初始渲染时不需要
    address: '123 Main St'
  });

  const updateUser = () => {
    setUser(prevUser => ({
     ...prevUser,
      age: prevUser.age + 1
    }));
  };

  return (
    <div>
      <p>Name: {user.name}, Age: {user.age}</p>
      <button onClick={updateUser}>Update User</button>
    </div>
  );
};

// 改进后的例子
import React, { useState } from'react';

const GoodInitialStateComponent = () => {
  const [user, setUser] = useState({
    name: 'John',
    age: 30
  });

  const updateUser = () => {
    setUser(prevUser => ({
     ...prevUser,
      age: prevUser.age + 1
    }));
  };

  return (
    <div>
      <p>Name: {user.name}, Age: {user.age}</p>
      <button onClick={updateUser}>Update User</button>
    </div>
  );
};

在反例中,初始 state 包含了 address 属性,即使这个属性在初始渲染和后续更新中可能并不需要,它的存在可能会导致不必要的重新渲染。在改进后的例子中,去掉了不必要的初始 state 属性,提升了性能。

8. 代码分割(Code Splitting)

随着应用程序的增长,代码体积也会增大,这可能会导致加载时间变长。代码分割是一种优化技术,它允许我们将代码分割成多个块,然后按需加载。在 React 中,可以使用动态 import() 来实现代码分割。

import React, { useState, lazy, Suspense } from'react';

const BigComponent = lazy(() => import('./BigComponent'));

const CodeSplittingComponent = () => {
  const [showBigComponent, setShowBigComponent] = useState(false);

  return (
    <div>
      <button onClick={() => setShowBigComponent(!showBigComponent)}>
        {showBigComponent? 'Hide Big Component' : 'Show Big Component'}
      </button>
      {showBigComponent && (
        <Suspense fallback={<div>Loading...</div>}>
          <BigComponent />
        </Suspense>
      )}
    </div>
  );
};

export default CodeSplittingComponent;

在这个例子中,BigComponent 是一个较大的组件,通过 lazy 和动态 import() 进行代码分割。只有当点击按钮显示 BigComponent 时,才会加载它的代码,而不是在应用启动时就加载所有代码,提高了应用的初始加载性能。

9. 使用 Context 时的优化

Context 是 React 提供的一种跨组件传递数据的方式,但如果使用不当,可能会导致性能问题。因为当 Context 的值发生变化时,所有订阅了该 Context 的组件都会重新渲染。

import React, { createContext, useState, useContext } from'react';

const MyContext = createContext();

const Child = () => {
  const value = useContext(MyContext);
  return <p>{value}</p>;
};

const ParentWithContext = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  return (
    <MyContext.Provider value={name}>
      <Child />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </MyContext.Provider>
  );
};

export default ParentWithContext;

在这个例子中,Child 组件只依赖于 Context 中的 name 值。但是如果不进行优化,当 count 变化时,ParentWithContext 重新渲染,Context 的值也会重新生成(即使 name 没有变化),导致 Child 组件不必要的重新渲染。为了优化,可以使用 React.memo 包裹 Child 组件,并结合 useMemo 来 memoize Context 的值。

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

const MyContext = createContext();

const MemoizedChild = React.memo(() => {
  const value = useContext(MyContext);
  return <p>{value}</p>;
});

const ParentWithContextOptimized = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');

  const contextValue = useMemo(() => name, [name]);

  return (
    <MyContext.Provider value={contextValue}>
      <MemoizedChild />
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </MyContext.Provider>
  );
};

export default ParentWithContextOptimized;

在这个优化后的例子中,MemoizedChild 组件只会在 Context 的值真正变化时(即 name 变化时)才会重新渲染,避免了因 count 变化导致的不必要重新渲染。

10. 性能监控与分析

要优化 React State 的性能,首先需要了解性能瓶颈在哪里。可以使用浏览器的开发者工具,如 Chrome DevTools 的 Performance 面板来分析应用的性能。

  1. 录制性能数据:在 Chrome DevTools 中,打开 Performance 面板,点击录制按钮,然后在应用中执行一些操作,比如点击按钮、滚动列表等,之后停止录制。
  2. 分析性能数据:录制完成后,会生成一个性能时间轴,其中包含了各种事件,如渲染、脚本执行等。可以通过查看 Function Call Tree 来找出哪些函数执行时间较长,哪些组件重新渲染次数过多。例如,如果发现某个组件的 render 方法执行时间很长,就需要检查该组件是否在 render 方法中执行了复杂计算或者存在不必要的重新渲染。
  3. 使用 React Profiler:React 提供了 React Profiler 工具,可以更深入地分析 React 组件的性能。通过在应用中添加 Profiler 组件,可以获取每个组件渲染的时间和次数等详细信息,从而有针对性地进行优化。
import React, { Profiler } from'react';

const MyComponent = () => {
  return <p>My Component</p>;
};

const ProfilerComponent = () => {
  const onRender = (id, phase, actualTime, baseTime, startTime, commitTime) => {
    console.log(`Component ${id} render time: ${actualTime}`);
  };

  return (
    <Profiler id="MyComponentProfiler" onRender={onRender}>
      <MyComponent />
    </Profiler>
  );
};

export default ProfilerComponent;

在这个例子中,Profiler 组件会在 MyComponent 每次渲染时调用 onRender 回调函数,通过这个回调函数可以获取渲染相关的时间信息,帮助我们分析性能。

通过综合运用以上这些性能优化策略,可以显著提升 React 应用中 state 的性能,提高用户体验,使应用更加流畅和高效。无论是小型应用还是大型项目,这些策略都能在不同程度上发挥作用,开发者应根据具体情况选择合适的优化方法。