React 性能优化策略

PureComponent、React.memo、useMemo、useCallback 的使用场景和实践

问题

React 应用中如何避免不必要的重新渲染?常用的优化手段有哪些?

解答

1. PureComponent(类组件)

PureComponent 自动对 props 和 state 进行浅比较,避免不必要的渲染。

import React, { PureComponent, Component } from 'react';

// 普通组件:父组件更新时总会重新渲染
class RegularChild extends Component {
  render() {
    console.log('RegularChild 渲染');
    return <div>{this.props.name}</div>;
  }
}

// PureComponent:props 浅比较相等时跳过渲染
class PureChild extends PureComponent {
  render() {
    console.log('PureChild 渲染');
    return <div>{this.props.name}</div>;
  }
}

class Parent extends Component {
  state = { count: 0, name: 'Tom' };

  render() {
    return (
      <div>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Count: {this.state.count}
        </button>
        {/* 点击按钮时,RegularChild 会重新渲染,PureChild 不会 */}
        <RegularChild name={this.state.name} />
        <PureChild name={this.state.name} />
      </div>
    );
  }
}

2. React.memo(函数组件)

React.memo 是函数组件版本的 PureComponent

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

// 普通函数组件
const RegularChild = ({ name }) => {
  console.log('RegularChild 渲染');
  return <div>{name}</div>;
};

// memo 包裹:props 不变时跳过渲染
const MemoChild = memo(({ name }) => {
  console.log('MemoChild 渲染');
  return <div>{name}</div>;
});

// 自定义比较函数
const MemoChildCustom = memo(
  ({ user }) => {
    console.log('MemoChildCustom 渲染');
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true 表示 props 相等,跳过渲染
    return prevProps.user.id === nextProps.user.id;
  }
);

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

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <RegularChild name={name} />
      <MemoChild name={name} />
    </div>
  );
}

3. useMemo(缓存计算结果)

useMemo 缓存计算结果,依赖不变时返回缓存值。

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

function ExpensiveList({ items, filter }) {
  // 没有 useMemo:每次渲染都重新计算
  // const filteredItems = items.filter(item => item.includes(filter));

  // 使用 useMemo:只在 items 或 filter 变化时重新计算
  const filteredItems = useMemo(() => {
    console.log('执行过滤计算');
    return items.filter(item => item.includes(filter));
  }, [items, filter]);

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

function App() {
  const [filter, setFilter] = useState('');
  const [count, setCount] = useState(0);
  const items = ['apple', 'banana', 'orange', 'grape'];

  return (
    <div>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {/* 点击按钮不会触发过滤计算 */}
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveList items={items} filter={filter} />
    </div>
  );
}

4. useCallback(缓存函数引用)

useCallback 缓存函数引用,配合 memo 使用避免子组件重新渲染。

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

// memo 包裹的子组件
const Button = memo(({ onClick, children }) => {
  console.log(`Button "${children}" 渲染`);
  return <button onClick={onClick}>{children}</button>;
});

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

  // 没有 useCallback:每次渲染创建新函数,Button 总会重新渲染
  // const handleClick = () => setCount(count + 1);

  // 使用 useCallback:函数引用稳定,Button 不会因为 text 变化而重新渲染
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <p>Count: {count}</p>
      <Button onClick={handleClick}>增加</Button>
    </div>
  );
}

5. 避免内联函数和对象

内联函数和对象每次渲染都会创建新引用,导致子组件重新渲染。

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

const Child = memo(({ style, onClick }) => {
  console.log('Child 渲染');
  return <div style={style} onClick={onClick}>Child</div>;
});

// ❌ 错误示范
function BadParent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* 每次渲染都创建新的 style 对象和 onClick 函数 */}
      <Child
        style={{ color: 'red' }}
        onClick={() => console.log('clicked')}
      />
    </div>
  );
}

// ✅ 正确示范
function GoodParent() {
  const [count, setCount] = useState(0);

  // 缓存对象
  const style = useMemo(() => ({ color: 'red' }), []);
  // 缓存函数
  const handleClick = useCallback(() => console.log('clicked'), []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <Child style={style} onClick={handleClick} />
    </div>
  );
}

完整示例

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

// 列表项组件
const ListItem = memo(({ item, onRemove }) => {
  console.log(`ListItem ${item.id} 渲染`);
  return (
    <li>
      {item.text}
      <button onClick={() => onRemove(item.id)}>删除</button>
    </li>
  );
});

// 列表组件
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' },
    { id: 2, text: '写代码' },
  ]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState('');

  // 缓存删除函数
  const handleRemove = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  // 缓存添加函数
  const handleAdd = useCallback(() => {
    if (!input.trim()) return;
    setTodos(prev => [...prev, { id: Date.now(), text: input }]);
    setInput('');
  }, [input]);

  // 缓存过滤结果
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => todo.text.includes(filter));
  }, [todos, filter]);

  return (
    <div>
      <input
        value={input}
        onChange={e => setInput(e.target.value)}
        placeholder="添加待办"
      />
      <button onClick={handleAdd}>添加</button>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="搜索"
      />
      <ul>
        {filteredTodos.map(item => (
          <ListItem key={item.id} item={item} onRemove={handleRemove} />
        ))}
      </ul>
    </div>
  );
}

关键点

  • PureComponent 用于类组件,自动浅比较 props 和 state
  • React.memo 用于函数组件,可传入自定义比较函数
  • useMemo 缓存计算结果,避免重复计算
  • useCallback 缓存函数引用,配合 memo 使用才有意义
  • 避免内联 函数和对象作为 props,否则每次渲染都是新引用
  • 不要过度优化,先用 React DevTools Profiler 定位性能瓶颈