Back to Blog
· 3 min read

React 性能优化:防止组件重新渲染

React

在中大型项目中,经常看到父组件状态更新,子组件即使 props 没变也会重新渲染,这种不必要的渲染累积起来会显著影响性能。掌握防止重新渲染的技巧,是 React 性能优化的关键。

React 的重新渲染机制是性能的核心。理解何时以及如何防止不必要的渲染,能有效改善页面响应速度。

1. 重新渲染的触发条件

React 组件在以下情况会重新渲染:

  • state 变化
  • props 变化
  • 父组件重新渲染
graph TD
    A[组件渲染] --> B{状态变化?}
    B -->|是| C[重新渲染]
    B -->|否| D{Props 变化?}
    D -->|是| C
    D -->|否| E{父组件渲染?}
    E -->|是| F{浅比较不同?}
    E -->|否| G[不重新渲染]
    F -->|是| C
    F -->|否| G

2. React.memo

2.1 基本用法

React.memo 对函数组件进行浅比较:

function ChildComponent({ name, onClick }) {
  console.log('Child 渲染了');
  return <button onClick={onClick}>{name}</button>;
}

// 只有 props 变化时才重新渲染
const MemoizedChild = React.memo(ChildComponent);

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

  return (
    <div>
      <p>父组件计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      {/* count 变化时,MemoizedChild 不会重新渲染 */}
      <MemoizedChild name="子组件" onClick={() => {}} />
    </div>
  );
}

2.2 自定义比较函数

function MyComponent({ data }) {
  return <div>{data.value}</div>;
}

// 自定义比较逻辑
const MemoizedComponent = React.memo(MyComponent, (prevProps, nextProps) => {
  return prevProps.data.id === nextProps.data.id;
});

3. useMemo

3.1 缓存计算结果

function ExpensiveComponent({ list, filter }) {
  // 只有 list 或 filter 变化时才重新计算
  const filteredList = useMemo(() => {
    console.log('计算中...');
    return list.filter(item => item.name.includes(filter));
  }, [list, filter]);

  return (
    <ul>
      {filteredList.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

3.2 避免对象引用变化

function Component({ a, b }) {
  // ❌ 每次渲染都创建新对象
  const config = { mode: 'dark', size: 'large' };

  // ✅ 使用 useMemo 缓存对象
  const config = useMemo(() => ({
    mode: 'dark',
    size: 'large',
  }), []);

  // ✅ 数组同理
  const items = useMemo(() => [a, b], [a, b]);
}

4. useCallback

4.1 缓存回调函数

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

  // ❌ 每次渲染都创建新函数
  const handleClick = () => console.log('click');

  // ✅ 缓存回调函数
  const handleClick = useCallback(() => {
    console.log('click');
  }, []);

  // ✅ 带依赖的回调
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return <Child onClick={handleClick} onIncrement={handleIncrement} />;
}

4.2 配合 React.memo 使用

function List({ items, onItemClick }) {
  console.log('List 渲染');
  return (
    <ul>
      {items.map(item => (
        <ListItem
          key={item.id}
          item={item}
          onClick={onItemClick}
        />
      ))}
    </ul>
  );
}

const MemoizedList = React.memo(List);

function App() {
  const [query, setQuery] = useState('');
  const [items] = useState([{ id: 1, name: '苹果' }, { id: 2, name: '香蕉' }]);

  const handleClick = useCallback((id) => {
    console.log('点击了', id);
  }, []);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <MemoizedList items={items} onItemClick={handleClick} />
    </div>
  );
}

5. 类组件优化

5.1 shouldComponentUpdate

class Counter extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 只在 count 变化时更新
    return nextProps.count !== this.props.count;
  }

  render() {
    return <div>计数: {this.props.count}</div>;
  }
}

5.2 PureComponent

class UserList extends React.PureComponent {
  render() {
    // 自动进行浅比较
    return (
      <ul>
        {this.props.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

6. 常见陷阱

6.1 内联函数问题

function Component() {
  return (
    <div>
      {/* ❌ 每次渲染创建新函数 */}
      <Child onClick={() => handleClick(id)} />

      {/* ✅ 使用 useCallback */}
      <Child onClick={useCallback(() => handleClick(id), [id])} />
    </div>
  );
}

6.2 数组和对象字面量

function Component({ items }) {
  return (
    <div>
      {/* ❌ 新数组引用 */}
      <Child items={[...items, newItem]} />

      {/* ✅ 使用 useMemo */}
      <Child items={useMemo(() => [...items, newItem], [items, newItem])} />
    </div>
  );
}

7. 性能优化 Checklist

优化方法适用场景优先级
React.memo函数组件
useMemo缓存计算结果
useCallback缓存回调函数
shouldComponentUpdate类组件
PureComponent类组件

8. 总结

防止不必要的重新渲染:

  • 优先使用 React.memo: 函数组件的首选优化方式
  • 合理使用 useMemo/useCallback: 不要过度使用,否则会增加内存开销
  • 避免内联函数和对象: 组件外部定义或使用 Hook 缓存

提示: 优化前先用 React DevTools 的 Profiler 确认性能瓶颈,避免过早优化。


相关阅读