如何提高组件的渲染效率?如何避免不必要的 render?

一、为什么需要优化

React 的默认行为是:当组件状态变化时,该组件及其所有子组件都会重新执行 render。对于大多数应用来说这不会有问题,但当组件树很深或 render 计算量较大时,不必要的重新渲染会导致界面卡顿。

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>计数:{count}</button>
      {/* 每次 count 变化,ExpensiveList 都会重新渲染 */}
      <ExpensiveList items={staticItems} />
    </div>
  );
}

优化的核心原则:让没有变化的组件跳过 render,让必须 render 的组件尽快完成。

二、React.memo — 函数组件记忆化

React.memo 是一个高阶组件,它对 props 进行浅比较,如果 props 没有变化则跳过重新渲染:

const ExpensiveList = React.memo(function ExpensiveList({
  items,
}: {
  items: Item[];
}) {
  console.log('ExpensiveList render');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

也可以传入自定义比较函数,对特定字段进行精确比较。

memo 的失效场景

React.memo 进行的是浅比较,以下情况会导致它失效:

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>

      {/* 每次渲染都创建新对象,memo 浅比较总是不等 */}
      <MemoChild style={{ color: 'red' }} />

      {/* 每次渲染都创建新函数引用 */}
      <MemoChild onClick={() => console.log('click')} />

      {/* 每次渲染都创建新数组 */}
      <MemoChild items={[1, 2, 3]} />
    </div>
  );
}

解决方案是配合 useMemouseCallback 稳定引用。

三、useMemo 与 useCallback — 稳定 Props 引用

useCallback 稳定函数引用

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

  const handleSearch = useCallback((term: string) => {
    fetchResults(term);
  }, []);

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

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>计数:{count}</button>
      <SearchBar onSearch={handleSearch} />
      <ItemList items={items} onItemClick={handleItemClick} />
    </div>
  );
}

const SearchBar = React.memo(function SearchBar({
  onSearch,
}: {
  onSearch: (term: string) => void;
}) {
  const [term, setTerm] = useState('');
  return (
    <input
      value={term}
      onChange={e => setTerm(e.target.value)}
      onKeyDown={e => e.key === 'Enter' && onSearch(term)}
    />
  );
});

useMemo 稳定对象/计算结果

function Dashboard({ data }: { data: RawData[] }) {
  const [filter, setFilter] = useState('all');

  const processedData = useMemo(() => {
    return data
      .filter(item => filter === 'all' || item.category === filter)
      .map(item => ({ ...item, score: computeScore(item) }))
      .sort((a, b) => b.score - a.score);
  }, [data, filter]);

  const chartConfig = useMemo(() => ({
    type: 'bar' as const,
    colors: ['#3b82f6', '#ef4444', '#22c55e'],
    animate: true,
  }), []);

  return (
    <div>
      <FilterBar value={filter} onChange={setFilter} />
      <DataTable data={processedData} />
      <Chart config={chartConfig} data={processedData} />
    </div>
  );
}

四、shouldComponentUpdate 与 PureComponent

在类组件中,可以通过 shouldComponentUpdate 手动控制是否需要重新渲染:

class HeavyList extends React.Component<Props, State> {
  shouldComponentUpdate(nextProps: Props, nextState: State) {
    return (
      nextProps.items !== this.props.items ||
      nextState.selectedId !== this.state.selectedId
    );
  }

  render() {
    return (
      <ul>
        {this.props.items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    );
  }
}

React.PureComponent 内置了浅比较的 shouldComponentUpdate,等价于对所有 props 和 state 进行浅层相等检查:

class OptimizedCard extends React.PureComponent<{ title: string; count: number }> {
  render() {
    return (
      <div>
        <h2>{this.props.title}</h2>
        <span>{this.props.count}</span>
      </div>
    );
  }
}

五、状态下放(State Colocation)

将状态尽可能放在需要它的组件附近,而非提升到不必要的高层级。这样状态变化时只有相关组件重新渲染:

// 优化前:所有子组件都因 inputValue 变化而重新渲染
function Page() {
  const [inputValue, setInputValue] = useState('');
  return (
    <div>
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <ExpensiveChart />
      <ExpensiveTable />
    </div>
  );
}

// 优化后:将输入状态下放到独立组件
function Page() {
  return (
    <div>
      <SearchInput />
      <ExpensiveChart />
      <ExpensiveTable />
    </div>
  );
}

function SearchInput() {
  const [inputValue, setInputValue] = useState('');
  return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
}

六、Children as Props 模式

利用 children 或 render props 将不变的组件树作为 props 传入,使其在父组件 state 变化时不被重新创建:

function ScrollTracker({ children }: { children: React.ReactNode }) {
  const [scrollY, setScrollY] = useState(0);

  useEffect(() => {
    const handler = () => setScrollY(window.scrollY);
    window.addEventListener('scroll', handler);
    return () => window.removeEventListener('scroll', handler);
  }, []);

  return (
    <div>
      <div className="scroll-indicator">滚动位置:{scrollY}px</div>
      {children}
    </div>
  );
}

// 使用时 children 不会因 scrollY 变化而重新渲染
function App() {
  return (
    <ScrollTracker>
      <ExpensiveContent />
    </ScrollTracker>
  );
}

原理:children 是在 App 渲染时创建的 JSX,ScrollTracker 的 state 变化不会导致 App 重新渲染,因此 children 引用保持不变。

七、Context 拆分

当一个 Context 包含多个值时,其中任意值变化会导致所有消费者重新渲染。将不同更新频率的数据拆分到不同的 Context 中:

// 优化前:theme 和 user 放在一起,更新任一都导致所有消费者渲染
const AppContext = createContext({ theme: 'light', user: null });

// 优化后:拆分为独立 Context
const ThemeContext = createContext<'light' | 'dark'>('light');
const UserContext = createContext<User | null>(null);

function AppProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const [user, setUser] = useState<User | null>(null);

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={user}>
        {children}
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

八、lazy 与 Suspense — 代码分割

通过 React.lazy 按需加载组件,减少初始包体积,加快首屏渲染:

import { lazy, Suspense } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));
const UserProfile = lazy(() => import('./UserProfile'));
const Analytics = lazy(() => import('./Analytics'));

function App() {
  return (
    <Suspense fallback={<LoadingSkeleton />}>
      <Routes>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/profile" element={<UserProfile />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

Suspense 还支持嵌套使用,为不同区域提供独立的加载状态和加载粒度控制。

九、React Compiler(自动优化)

React 19 引入的 React Compiler(原 React Forget)可以在编译时自动插入 memo/useMemo/useCallback 等优化,让开发者无需手动管理引用稳定性:

// 编写时无需手动 memo
function TodoList({ todos, onToggle }) {
  const visibleTodos = todos.filter(t => !t.hidden);

  return (
    <ul>
      {visibleTodos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
      ))}
    </ul>
  );
}

// React Compiler 编译后自动等效于:
// - visibleTodos 被 useMemo 包裹
// - onToggle 引用自动追踪
// - TodoItem 渲染自动优化

React Compiler 通过静态分析组件代码,自动识别哪些值需要记忆化,从根本上解决了手动优化的负担和遗漏问题。

十、总结

优化手段适用场景原理
React.memo函数组件 props 稳定时浅比较 props,跳过 render
useMemo计算结果 / 对象引用缓存值,依赖不变则返回旧值
useCallback函数 props缓存函数引用
状态下放局部状态影响范围过大缩小状态变化的影响范围
Children 模式父组件频繁更新不变的子树作为 props 传入
Context 拆分Context 消费者过多减少不相关消费者的更新
lazy / Suspense大型路由 / 重组件按需加载,减少初始 render 量
React Compiler全局自动优化编译时自动插入记忆化

优化的首要原则是先度量再优化。使用 React DevTools Profiler 定位实际的性能瓶颈,避免过早优化带来的代码复杂性。