Hook18.0+

useTransition

标记非紧急的状态更新,让 React 在并发渲染中保持界面响应性

核心概述

痛点: UI 阻塞问题

在 React 18 之前,所有状态更新都是"紧急"的(Urgent),这导致:

  • 输入卡顿: 当用户在输入框打字时,如果同时有大量列表渲染, 会导致输入延迟(用户按键 → 字符显示在屏幕上有明显延迟)
  • 点击无响应: 点击按钮后,如果触发重型计算或渲染, 界面会"冻结",用户无法看到点击反馈
  • 动画掉帧: 正在进行的动画会因为其他高优先级更新而中断

解决方案: 并发渲染与 Transition

React 18 引入并发渲染(Concurrent Rendering), 允许 React 同时准备多个版本的 UI,根据优先级选择显示哪个版本。

useTransition 利用并发渲染能力,将状态更新标记为"非紧急"(Non-urgent):

  • 优先级分级: 区分紧急更新(如打字、点击)和非紧急更新(如搜索结果、Tab 切换)
  • 可中断渲染: 非紧急更新可以被更高优先级的更新打断,保持界面响应
  • 并发特性: React 在浏览器空闲时计算 transition 更新,不影响用户交互

适用场景

  • 搜索/过滤: 输入框立即更新,搜索结果延迟计算
  • Tab 切换: 点击立即响应,内容延迟加载
  • 列表展开: 展开/折叠图标立即变化,子节点延迟渲染
  • 数据可视化: 过滤器立即应用,图表延迟重绘

💡 心智模型

将 useTransition 想象成"快递加急通道":

  • 加急通道(紧急更新): 用户打字、点击等高优先级任务走加急通道, 立即处理并显示
  • 普通通道(Transition): 搜索结果、Tab 内容等非紧急任务走普通通道, 在加急任务空闲时处理
  • 可插队机制: 如果加急通道有新任务,普通通道的任务暂停, 优先处理加急任务
  • 完成标志: isPending 告诉你普通通道是否有任务正在处理

关键: transition 中的更新不是"异步执行"(像 setTimeout), 而是"低优先级执行"(可以在浏览器空闲时处理,但会被紧急更新打断)。

技术规格

类型签名

function useTransition(): [
  boolean,
  (callback: () => void) => void
]

返回值

返回值类型说明
isPendingboolean是否有 transition 正在处理。true 表示有 pending transition
startTransition(callback: () => void) => void标记状态更新为 transition 的函数,接受一个包含 setState 调用的回调

运行机制

useTransition 基于 React 18 的并发渲染特性,底层机制如下:

  • 优先级调度: React 使用 Lane 模型管理更新优先级, transition 更新被分配较低的优先级
  • 时间切片: React 将渲染工作分解成小单元, 在每个时间切片后检查是否有更高优先级的更新
  • 可中断渲染: 如果在 transition 渲染过程中有紧急更新(如用户输入), React 会放弃当前的 transition 渲染,处理紧急更新后再重新开始 transition
  • 并发协调: React 可以同时维护多个版本的 UI 树, 根据优先级选择显示哪个版本

注意: useTransition 需要 React 18+ 和支持并发渲染的渲染器 (如 ReactDOM 18+ 的 createRoot)。使用旧版 ReactDOM.render() 不会启用并发特性。

实战演练

示例 1: 搜索输入框(基础用法)

最常见的场景:用户输入立即显示,搜索结果延迟计算。

import { useState, useTransition } from 'react';

function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    // ✅ 紧急更新: 立即更新输入框(用户需要看到自己输入的字符)
    setQuery(value);

    // ⏳ 非紧急更新: 搜索结果可以延迟(用户可以接受短暂等待)
    startTransition(() => {
      const filtered = items.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="搜索..."
        className="border p-2 rounded"
      />
      {isPending && (
        <p className="text-sm text-gray-500 mt-2">搜索中...</p>
      )}
      <ul className="mt-4 space-y-1">
        {filteredItems.map(item => (
          <li key={item} className="text-sm">{item}</li>
        ))}
      </ul>
    </div>
  );
}

效果: 用户打字时,输入框立即响应,即使 items 数组很大(如 10,000 条), 也不会感到卡顿。搜索结果会在浏览器空闲时更新。

示例 2: Tab 切换(生产级,结合 Suspense)

Tab 切换是另一个典型场景:点击立即响应,内容延迟加载。

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

// 模拟异步数据获取
async function fetchTabData(tabName: string): Promise<string[]> {
  // 假设这是一个 API 调用
  await new Promise(resolve => setTimeout(resolve, 1000));
  return [`${tabName} 数据 1`, `${tabName} 数据 2`, `${tabName} 数据 3`];
}

// Tab 内容组件(使用 Suspense)
function TabContent({ tabName }: { tabName: string }) {
  // 抛出 Promise 触发 Suspense
  const data = fetchTabData(tabName);
  throw data;

  // 实际项目中会用 use() Hook 或数据获取库
  // return <div>{data.map(...)}</div>;
}

function TabContainer() {
  const [activeTab, setActiveTab] = useState<'home' | 'posts' | 'about'>('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (tab: 'home' | 'posts' | 'about') => {
    startTransition(() => {
      // ⏳ Tab 切换标记为 transition,内容加载不会阻塞点击反馈
      setActiveTab(tab);
    });
  };

  return (
    <div>
      {/* Tab 按钮 */}
      <div className="flex gap-2 mb-4">
        {(['home', 'posts', 'about'] as const).map(tab => (
          <button
            key={tab}
            onClick={() => switchTab(tab)}
            className={`px-4 py-2 rounded ${
              activeTab === tab
                ? 'bg-blue-500 text-white'
                : 'bg-gray-200 text-gray-700'
            }`}
          >
            {tab.charAt(0).toUpperCase() + tab.slice(1)}
            {isPending && activeTab === tab && (
              <span className="ml-2">加载中...</span>
            )}
          </button>
        ))}
      </div>

      {/* Tab 内容(带 Suspense) */}
      <Suspense fallback={<div className="p-4">加载中...</div>}>
        {activeTab === 'home' && <div>首页内容</div>}
        {activeTab === 'posts' && <div>文章内容</div>}
        {activeTab === 'about' && <div>关于内容</div>}
      </Suspense>
    </div>
  );
}

效果: 点击 Tab 按钮时,按钮样式立即变化(用户得到反馈), 内容区域显示加载状态,数据加载完成后显示内容。整个过程流畅,不会"卡住"界面。

示例 3: 树形列表展开(复杂场景)

树形结构展开时,可能需要渲染大量子节点。使用 transition 可以保持交互流畅。

import { useState, useTransition } from 'react';

interface TreeNode {
  id: string;
  name: string;
  children?: TreeNode[];
}

function TreeView({ data }: { data: TreeNode[] }) {
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
  const [isPending, startTransition] = useTransition();

  const toggleNode = (nodeId: string) => {
    // ✅ 立即更新展开/折叠状态(视觉反馈)
    const nextExpandedIds = new Set(expandedIds);
    if (nextExpandedIds.has(nodeId)) {
      nextExpandedIds.delete(nodeId);
    } else {
      nextExpandedIds.add(nodeId);
    }

    // ⏳ 延迟渲染子节点(可能很大量)
    startTransition(() => {
      setExpandedIds(nextExpandedIds);
    });
  };

  return (
    <ul className="space-y-1">
      {data.map(node => (
        <TreeNode
          key={node.id}
          node={node}
          expandedIds={expandedIds}
          onToggle={toggleNode}
          isPending={isPending}
        />
      ))}
    </ul>
  );
}

function TreeNode({
  node,
  expandedIds,
  onToggle,
  isPending
}: {
  node: TreeNode;
  expandedIds: Set<string>;
  onToggle: (id: string) => void;
  isPending: boolean;
}) {
  const isExpanded = expandedIds.has(node.id);
  const hasChildren = node.children && node.children.length > 0;

  return (
    <li>
      <div
        onClick={() => onToggle(node.id)}
        className="flex items-center gap-2 cursor-pointer hover:bg-gray-100 p-1 rounded"
      >
        {hasChildren && (
          <span className="text-gray-500">
            {isExpanded ? '▼' : '▶'}
          </span>
        )}
        <span>{node.name}</span>
        {isPending && isExpanded && (
          <span className="text-xs text-gray-400">展开中...</span>
        )}
      </div>

      {isExpanded && hasChildren && (
        <ul className="ml-6 mt-1 space-y-1">
          {node.children!.map(child => (
            <TreeNode
              key={child.id}
              node={child}
              expandedIds={expandedIds}
              onToggle={onToggle}
              isPending={isPending}
            />
          ))}
        </ul>
      )}
    </li>
  );
}

// 使用示例
const treeData: TreeNode[] = [
  {
    id: '1',
    name: '根节点',
    children: [
      {
        id: '1-1',
        name: '子节点 1',
        children: Array.from({ length: 100 }, (_, i) => ({
          id: `1-1-${i}`,
          name: `深层节点 ${i}`
        }))
      },
      {
        id: '1-2',
        name: '子节点 2',
        children: Array.from({ length: 100 }, (_, i) => ({
          id: `1-2-${i}`,
          name: `深层节点 ${i}`
        }))
      }
    ]
  }
];

// <TreeView data={treeData} />

效果: 点击展开图标时,图标立即旋转(用户得到反馈), 子节点在浏览器空闲时渲染。即使有数千个子节点,也不会阻塞界面。

示例 4: 数据可视化仪表板(生产级)

在数据可视化中,过滤器和图表都很"重",使用 transition 分离紧急和非紧急更新。

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

interface DataPoint {
  id: string;
  category: string;
  value: number;
  timestamp: number;
}

interface Filters {
  category?: string;
  dateRange?: [number, number];
  minValue?: number;
}

function Dashboard({ rawData }: { rawData: DataPoint[] }) {
  const [filters, setFilters] = useState<Filters>({});
  const [filteredData, setFilteredData] = useState<DataPoint[]>(rawData);
  const [isPending, startTransition] = useTransition();

  // 应用过滤器(可能在大量数据上执行复杂计算)
  const applyFilters = (newFilters: Filters) => {
    let result = rawData;

    if (newFilters.category) {
      result = result.filter(d => d.category === newFilters.category);
    }

    if (newFilters.dateRange) {
      const [start, end] = newFilters.dateRange;
      result = result.filter(d => d.timestamp >= start && d.timestamp <= end);
    }

    if (newFilters.minValue !== undefined) {
      result = result.filter(d => d.value >= newFilters.minValue!);
    }

    return result;
  };

  const handleFilterChange = (key: keyof Filters, value: any) => {
    const newFilters = { ...filters, [key]: value };

    // ✅ 立即更新过滤器 UI(选中状态、下拉框显示等)
    setFilters(newFilters);

    // ⏳ 延迟数据过滤和图表重绘(可能很耗时)
    startTransition(() => {
      const filtered = applyFilters(newFilters);
      setFilteredData(filtered);
    });
  };

  // 计算统计数据(也放在 transition 中)
  const stats = useMemo(() => {
    const total = filteredData.length;
    const sum = filteredData.reduce((acc, d) => acc + d.value, 0);
    const avg = total > 0 ? sum / total : 0;

    return { total, sum, avg };
  }, [filteredData]);

  return (
    <div className="p-4">
      {/* 过滤器面板 */}
      <div className="mb-6 p-4 bg-gray-50 rounded">
        <h3 className="font-semibold mb-3">过滤器</h3>

        <div className="space-y-3">
          <div>
            <label className="block text-sm mb-1">类别</label>
            <select
              value={filters.category || ''}
              onChange={(e) => handleFilterChange('category', e.target.value || undefined)}
              className="w-full border p-2 rounded"
            >
              <option value="">全部</option>
              <option value="A">类别 A</option>
              <option value="B">类别 B</option>
              <option value="C">类别 C</option>
            </select>
          </div>

          <div>
            <label className="block text-sm mb-1">最小值</label>
            <input
              type="number"
              value={filters.minValue || ''}
              onChange={(e) => handleFilterChange('minValue', Number(e.target.value) || undefined)}
              className="w-full border p-2 rounded"
              placeholder="输入最小值..."
            />
          </div>
        </div>
      </div>

      {/* 加载状态 */}
      {isPending && (
        <div className="mb-4 p-2 bg-yellow-50 text-yellow-700 text-sm rounded">
          更新图表中...
        </div>
      )}

      {/* 统计数据 */}
      <div className="mb-6 p-4 bg-blue-50 rounded grid grid-cols-3 gap-4">
        <div>
          <div className="text-sm text-gray-600">总记录数</div>
          <div className="text-2xl font-bold">{stats.total}</div>
        </div>
        <div>
          <div className="text-sm text-gray-600">总和</div>
          <div className="text-2xl font-bold">{stats.sum.toFixed(2)}</div>
        </div>
        <div>
          <div className="text-sm text-gray-600">平均值</div>
          <div className="text-2xl font-bold">{stats.avg.toFixed(2)}</div>
        </div>
      </div>

      {/* 图表区域 */}
      <div className="p-4 bg-white border rounded">
        <h3 className="font-semibold mb-3">数据可视化</h3>
        <div className="h-64 flex items-center justify-center text-gray-400">
          {/* 实际项目中这里会是图表库(如 Recharts、Chart.js 等) */}
          图表区域 ({filteredData.length} 条数据)
        </div>
      </div>
    </div>
  );
}

效果: 用户调整过滤器时,UI 立即响应(下拉框关闭、输入框显示值), 数据过滤、图表重绘在后台进行。即使数据量大、计算复杂,界面始终保持可交互。

避坑指南

❌ 陷阱 1: 将文本输入框的更新标记为 Transition

问题: 文本输入需要立即更新,否则会感觉"延迟"或"卡顿"。 将输入更新放在 transition 中会导致打字不跟手。

// ❌ 错误: 文本输入不应该用 transition
function SearchInput() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    startTransition(() => {
      setQuery(e.target.value); // ❌ 会导致打字延迟!
    });
  };

  return <input value={query} onChange={handleChange} />;
}

// ✅ 正确: 文本输入立即更新,搜索结果延迟更新
function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    // ✅ 立即更新输入框
    setQuery(value);

    // ✅ 搜索结果用 transition
    startTransition(() => {
      const filtered = items.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results items={filteredItems} />}
    </div>
  );
}

心智模型纠正: 文本输入是"紧急更新"(用户需要立即看到反馈), 只有搜索结果、Tab 内容等才是"非紧急更新"。

❌ 陷阱 2: 在 Transition 回调中执行副作用

问题: Transition 回调应该只包含状态更新, 不应该包含副作用(如直接修改 DOM、调用 API、记录日志等)。

// ❌ 错误: 在 transition 中执行副作用
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab);
      // ❌ 不要在 transition 中做这些:
      document.title = newTab; // 直接修改 DOM
      console.log('Switched to', newTab); // 记录日志
      fetch('/api/track', { // API 调用
        method: 'POST',
        body: JSON.stringify({ tab: newTab })
      });
    });
  };

  return <div>...</div>;
}

// ✅ 正确: 副作用放在 useEffect 中
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab); // ✅ 只包含状态更新
    });
  };

  // ✅ 副作用放在 useEffect 中
  useEffect(() => {
    document.title = tab;
  }, [tab]);

  useEffect(() => {
    console.log('Switched to', tab);
    fetch('/api/track', {
      method: 'POST',
      body: JSON.stringify({ tab })
    });
  }, [tab]);

  return <div>...</div>;
}

心智模型纠正: Transition 只用于标记"状态更新的优先级", 不用于管理"副作用"。副作用应该放在 useEffect 中。

❌ 陷阱 3: 期望 Transition 减少计算量或渲染次数

问题: Transition 不会减少计算量或跳过渲染, 它只是调整渲染的"优先级"和"时机",让紧急更新可以先处理。

// ❌ 错误理解: 认为 transition 会"跳过"计算
function ExpensiveList({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (value: string) => {
    setFilter(value);

    startTransition(() => {
      // ❌ 错误理解: 以为这样会"减少"计算
      // 实际上: 完整的过滤计算还是会执行,只是优先级较低
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(filtered);
    });
  };

  return <div>...</div>;
}

// ✅ 正确理解: Transition 保持响应性,不减少计算
// 如果真的要减少计算,应该用防抖、useMemo 或 Web Worker
function ExpensiveList({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('');
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  // 方案 1: 使用 useMemo 缓存计算结果
  const filteredMemo = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  // 方案 2: 使用防抖减少计算频率
  const debouncedFilter = useDebounce(filter, 300);

  useEffect(() => {
    const filtered = items.filter(item =>
      item.name.toLowerCase().includes(debouncedFilter.toLowerCase())
    );
    setFiltered(filtered);
  }, [items, debouncedFilter]);

  // 方案 3: Transition + useMemo(推荐)
  const handleChange = (value: string) => {
    setFilter(value); // 立即更新

    startTransition(() => {
      // ⏳ 延迟更新,但计算量没变
      // 好处: 用户可以继续输入,不会感觉卡顿
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(filtered);
    });
  };

  return <div>...</div>;
}

心智模型纠正: Transition 是"优先级管理工具",不是"性能优化工具"。 它让界面保持响应,但不减少计算量。要真正减少计算,需要用防抖、useMemo、Web Worker 等。

❌ 陷阱 4: 过度使用 Transition

问题: 不是所有状态更新都需要 transition。 简单的、快速的更新用 transition 反而增加复杂度。

// ❌ 错误: 简单更新也用 transition
function SimpleButton() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(() => {
      // ❌ 这个更新很快,不需要 transition
      setCount(c => c + 1);
    });
  };

  return (
    <button onClick={handleClick}>
      {isPending ? '更新中...' : `点击了 ${count} 次`}
    </button>
  );
}

// ✅ 正确: 简单更新直接更新
function SimpleButton() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // ✅ 简单的计数器更新,不需要 transition
    setCount(c => c + 1);
  };

  return <button>点击了 {count} 次</button>;
}

// ✅ 正确: 只有复杂的、可能阻塞的更新才用 transition
function ComplexButton({ items }: { items: Item[] }) {
  const [selectedItem, setSelectedItem] = useState<Item | null>(null);
  const [details, setDetails] = useState<Details | null>(null);
  const [isPending, startTransition] = useTransition();

  const handleItemClick = (item: Item) => {
    // ✅ 立即更新选中状态(视觉反馈)
    setSelectedItem(item);

    // ✅ 延迟计算详细信息(可能很耗时)
    startTransition(() => {
      const details = calculateExpensiveDetails(item);
      setDetails(details);
    });
  };

  return (
    <div>
      {items.map(item => (
        <div
          key={item.id}
          onClick={() => handleItemClick(item)}
          className={selectedItem === item ? 'selected' : ''}
        >
          {item.name}
        </div>
      ))}
      {isPending && <Spinner />}
      {details && <DetailsPanel details={details} />}
    </div>
  );
}

心智模型纠正: Transition 用于"可能阻塞交互的复杂更新", 简单的、快速的更新直接用 setState 即可。

最佳实践

✅ 推荐模式

1. 分离紧急和非紧急更新

这是使用 useTransition 的核心原则:将直接影响用户体验的更新(输入、点击反馈) 和可以延迟的更新(搜索结果、内容加载)分开。

const handleInput = (value: string) => {
  // ✅ 紧急更新: 用户需要立即看到
  setInputValue(value);

  // ✅ 非紧急更新: 可以延迟
  startTransition(() => {
    setFilteredData(filterData(value));
  });
};

2. 结合 Suspense 显示加载状态

Transition 与 Suspense 配合使用,可以优雅地处理异步数据加载。

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const switchTab = (newTab: string) => {
    startTransition(() => {
      setTab(newTab);
    });
  };

  return (
    <div>
      <button onClick={() => switchTab('posts')}>文章</button>
      {isPending && <Spinner />}

      <Suspense fallback={<div className="p-4">加载中...</div>}>
        {tab === 'home' && <Home />}
        {tab === 'posts' && <Posts />}
      </Suspense>
    </div>
  );
}

3. 使用 isPending 显示加载状态

利用 isPending 为用户提供视觉反馈,告诉他们"系统正在处理"。

function SearchInput({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (value: string) => {
    setQuery(value);
    startTransition(() => {
      const filtered = items.filter(item =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleChange(e.target.value)} />
      {/* ✅ 显示加载状态 */}
      {isPending && (
        <div className="mt-2 text-sm text-gray-500 flex items-center gap-2">
          <Spinner className="w-4 h-4" />
          <span>搜索中...</span>
        </div>
      )}
      <ul className="mt-2">
        {filteredItems.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

4. 与其他性能优化工具配合

Transition 可以与 useMemo、useCallback、防抖等工具配合使用, 达到最佳性能。

function OptimizedSearch({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  // ✅ 使用防抖减少计算频率
  const debouncedQuery = useDebounce(query, 300);

  // ✅ 防抖后再用 transition 保持响应性
  useEffect(() => {
    startTransition(() => {
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(debouncedQuery.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  }, [items, debouncedQuery]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ✅ 立即更新输入框
    setQuery(e.target.value);
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <Results items={filteredItems} />
    </div>
  );
}

⚠️ 使用建议

  • 先确保正确,再考虑性能: 不要一开始就用 useTransition, 先实现功能,如果发现卡顿再优化
  • 测量性能: 使用 React DevTools Profiler 测量渲染时间, 确认瓶颈确实是由渲染导致的
  • 渐进式优化: 一次只优化一个部分, 观察 effect,避免过度优化
  • 设备测试: 在不同性能的设备上测试, 确保在低端设备上也有改善

📊 useTransition vs useDeferredValue

React 提供了两个类似的并发特性工具,使用场景略有不同:

场景useTransitionuseDeferredValue
主要用途标记状态更新为低优先级延迟某个值的渲染
控制粒度控制一组状态更新控制单个值的渲染时机
适用场景搜索、Tab 切换、过滤等用户触发的更新从 props 派生的复杂 UI、需要延迟渲染的部分
典型代码startTransition(() => setState(...))const deferred = useDeferredValue(value)

推荐: 大多数情况下,如果要在用户触发的回调中包装 setState, 使用 useTransition。如果要延迟渲染从 props/state 派生的值, 使用 useDeferredValue。两者可以结合使用。

延伸阅读