React 性能优化(下):useCallback 与 useTransition 实战

0 阅读3分钟

引言

在 React 应用性能优化中,useCallbackuseTransition 是两个强大但常被误解的 Hook。本文将深入探讨它们的正确使用场景、常见陷阱以及实际代码示例,帮助你写出更高效的 React 应用。

useCallback:避免不必要的函数重建

核心原理

useCallback 返回一个记忆化的回调函数,只有在依赖项变化时才会重新创建。这对于避免子组件不必要的重新渲染至关重要。

基础用法

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

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

  // ❌ 错误:每次渲染都会创建新函数
  const handleClick = () => {
    console.log('Count:', count);
  };

  // ✅ 正确:使用 useCallback 记忆化
  const handleMemoizedClick = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <div>
      <button onClick={handleMemoizedClick}>点击</button>
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

配合 React.memo 使用

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

const ChildComponent = memo(({ onIncrement, value }) => {
  console.log('Child rendered');
  return (
    <button onClick={onIncrement}>
      Count: {value}
    </button>
  );
});

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

  // ✅ 只有 count 变化时,onIncrement 才会变化
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <ChildComponent onIncrement={handleIncrement} value={count} />
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

常见陷阱

// ❌ 陷阱 1:依赖项过多导致失去优化效果
const handler = useCallback(() => {
  doSomething(a, b, c, d, e);
}, [a, b, c, d, e]); // 几乎每次都会重新创建

// ✅ 解决:只依赖真正需要的变量
const handler = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// ❌ 陷阱 2:在 useCallback 内部使用非稳定引用
const handler = useCallback(() => {
  config.doSomething(); // config 每次都是新对象
}, [config]);

// ✅ 解决:依赖稳定的值
const handler = useCallback(() => {
  configRef.current.doSomething();
}, []);

useTransition:优化耗时更新

核心概念

useTransition 允许你将某些状态更新标记为"过渡"更新,让 UI 保持响应式,避免阻塞用户交互。

基础用法

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

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value) => {
    setQuery(value);
    
    // ✅ 将耗时的过滤操作标记为过渡更新
    startTransition(() => {
      const filtered = heavyFilter(value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input 
        value={query}
        onChange={e => handleSearch(e.target.value)}
        placeholder="搜索..."
      />
      {isPending && <span>加载中...</span>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

function heavyFilter(query) {
  // 模拟耗时操作
  const data = largeDataset.filter(item => 
    item.name.includes(query)
  );
  return data;
}

实际场景:标签切换

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

function TabComponent() {
  const [activeTab, setActiveTab] = useState('posts');
  const [isPending, startTransition] = useTransition();

  const tabs = {
    posts: <PostsList />,
    comments: <CommentsList />,
    analytics: <AnalyticsPanel />
  };

  const handleTabChange = (tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  return (
    <div>
      <nav>
        {Object.keys(tabs).map(tab => (
          <button
            key={tab}
            onClick={() => handleTabChange(tab)}
            disabled={isPending}
          >
            {tab}
          </button>
        ))}
      </nav>
      {isPending && <div className="spinner">切换中...</div>}
      <main>
        {tabs[activeTab]}
      </main>
    </div>
  );
}

与 Suspense 配合

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

function Dashboard() {
  const [activeView, setActiveView] = useState('overview');
  const [isPending, startTransition] = useTransition();

  const handleViewChange = (view) => {
    startTransition(() => {
      setActiveView(view);
    });
  };

  return (
    <div>
      <TabNav onChange={handleViewChange} active={activeView} />
      <Suspense fallback={<LoadingSkeleton />}>
        <ViewContent view={activeView} />
      </Suspense>
    </div>
  );
}

性能对比实测

// 未优化的版本
function UnoptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  
  // 每次输入都会重新渲染整个列表
  const filtered = items.filter(item => 
    item.name.includes(filter)
  );

  return (
    <div>
      <input onChange={e => setFilter(e.target.value)} />
      <List data={filtered} />
    </div>
  );
}

// 优化后的版本
function OptimizedList({ items }) {
  const [filter, setFilter] = useState('');
  const [displayItems, setDisplayItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setFilter(value);
    
    startTransition(() => {
      const filtered = items.filter(item => 
        item.name.includes(value)
      );
      setDisplayItems(filtered);
    });
  };

  return (
    <div>
      <input value={filter} onChange={handleChange} />
      {isPending && <LoadingIndicator />}
      <List data={displayItems} />
    </div>
  );
}

最佳实践总结

useCallback 使用指南

  1. 优先优化子组件:只有当函数作为 prop 传递给 React.memo 组件时才需要
  2. 避免过早优化:不是所有函数都需要 useCallback
  3. 注意依赖项:确保依赖项稳定且必要
  4. 配合 useMemo:复杂计算场景结合使用

useTransition 使用指南

  1. 识别耗时更新:列表过滤、大数据渲染、复杂计算
  2. 提供加载反馈:使用 isPending 显示加载状态
  3. 区分优先级:用户输入立即响应,数据更新可延迟
  4. 避免滥用:简单更新不需要过渡

总结

useCallbackuseTransition 是 React 性能优化的利器,但需要正确使用:

  • useCallback 解决的是函数引用稳定性问题
  • useTransition 解决的是更新优先级问题

记住:性能优化应该基于实际测量,而非猜测。使用 React DevTools Profiler 找出真正的瓶颈,再针对性地应用这些 Hook。