React 18 useTransition

8 分钟
13 阅读
React 18 useTransition

useTransition 是 React 18 引入、在 React 19 中继续强化的一个 并发特性 Hook
它主要用于 在用户交互触发的更新中,区分紧急(urgent)更新与非紧急(transition)更新,从而优化渲染性能、避免界面卡顿。

下面我会从原理、语法、使用场景、实例代码、React 19 的增强几个方面,写成你可以直接放博客的详细内容👇


🚀 React 中的 useTransition 全面解析与实战指南

一、useTransition 是什么?

在 React 中,所有的状态更新默认都是同步且立即渲染的
但在复杂界面中,比如过滤列表、切换页面、渲染大数据时,如果都同步执行,UI 就容易“卡顿”。

于是 React 18 引入了 并发渲染(Concurrent Rendering) 概念,允许你把某些更新标记为“非紧急”,从而让 React 能优先处理更重要的交互。

useTransition() 就是这个机制的入口。


二、基本语法

tsx 复制代码
const [isPending, startTransition] = useTransition();
  • isPending:布尔值。当某个 transition 还没完成时为 true,可用来显示加载状态。
  • startTransition(callback):将回调函数包裹起来,告诉 React:
    👉 “这个更新不紧急,可以在空闲时再渲染。”

三、使用场景

场景 说明
🔍 搜索过滤 用户在输入框输入时,立即显示输入框内容,但延迟过滤逻辑,避免卡顿
📄 列表分页 快速切换页码时,先让按钮响应,再加载内容
🧭 路由切换 页面切换时优先保留旧界面,直到新页面准备好
🧩 大型渲染 组件层级深、渲染量大时,提升交互流畅度

四、基础用例示例

示例 1:搜索过滤优化

tsx 复制代码
import React, { useState, useTransition } from "react";

const SearchList = ({ items }) => {
  const [query, setQuery] = useState("");
  const [filtered, setFiltered] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);

    // 非紧急更新:过滤大列表
    startTransition(() => {
      const result = items.filter((item) =>
        item.toLowerCase().includes(value.toLowerCase())
      );
      setFiltered(result);
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      {isPending && <p>Filtering...</p>}

      <ul>
        {filtered.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default SearchList;

🔍 解释:

  • 用户打字时,输入框响应立即更新(紧急更新)。
  • 列表过滤属于次要任务,用 startTransition 包裹。
  • 当过滤还没完成时,isPendingtrue,显示 Loading。

五、与普通状态更新的区别

类型 方式 结果
普通更新 setState() React 马上重新渲染(可能阻塞 UI)
过渡更新 startTransition(() => setState()) React 先处理紧急任务,再渲染非紧急更新

React 在内部会使用 并发调度器(Scheduler) 管理这些更新,让交互优先执行,UI 更流畅。


六、React 19 的变化与优化

在 React 19 中,useTransition 得到了更深的整合和性能改进:

  1. 更智能的优先级调度

    • React 19 的 Scheduler 更准确地区分用户输入(例如键盘输入)与渲染任务。
    • startTransition 包裹的任务分配更低优先级,但中断与恢复更平滑。
  2. 支持异步组件加载(Suspense 配合)

    tsx 复制代码
    startTransition(() => {
      navigate("/dashboard");
    });
    • 在使用 React Router v7 等框架时,useTransition 可包裹 navigate,实现 路由级别的并发渲染
  3. useOptimistic 结合

    • React 19 新增 useOptimistic,可与 useTransition 一起用于乐观 UI 更新,在提交网络请求的同时先渲染“预期结果”。
    • 当请求失败时,再回退到旧状态,交互更自然。

    示例:

    tsx 复制代码
    const [isPending, startTransition] = useTransition();
    const [optimisticComments, addOptimisticComment] = useOptimistic(comments);
    
    const handleSubmit = async (text) => {
      startTransition(async () => {
        addOptimisticComment({ text, pending: true });
        await postComment(text);
      });
    };

七、最佳实践与注意事项

适用场景:

  • 任务较重但结果不影响用户操作;
  • 用户交互后页面渲染缓慢;
  • 你想在渲染过程中展示“过渡态”或“加载中”。

⚠️ 不要滥用:

  • 如果任务本身很轻(如修改文字或按钮状态),没必要使用;
  • 过渡更新不会保证立即完成,也不适合用于依赖实时性的逻辑(如计时器)。

八、总结

特性 说明
Hook 名 useTransition()
返回值 [isPending, startTransition]
作用 将部分更新标记为非紧急,提高交互流畅度
React 19 增强 更智能调度 + 与 useOptimistic 联动

在实际开发中感觉 startTransition 有无效果差异不明显,核心原因是 当前场景未触发「非紧急更新阻塞紧急更新」的性能瓶颈startTransition 的价值需要在特定条件下才会凸显,我们可以从「效果不明显的原因」和「如何复现其价值」两方面拆解:

一、为什么你感觉“没区别”?核心原因分析

startTransition 的本质是 将“非紧急更新”标记为低优先级,优先保证“紧急更新”(如输入、点击反馈)的流畅性。如果你的场景不满足以下「性能瓶颈条件」,自然感受不到差异:

1. 数据量/计算量不足,非紧急更新本身很快

startTransition 主要解决「大量数据处理导致主线程阻塞」的问题。如果你的 items 列表数据量很小(比如只有几十/几百条),filter 过滤操作能在 16ms 内完成(浏览器一帧的时间),主线程不会被阻塞,紧急更新(输入框 setQuery)和非紧急更新(setFiltered)会同步执行,自然看不出差异。

比如你之前的代码若 items 只有 100 条,过滤操作耗时可能只有 0.1ms,即使不包裹 startTransition,输入框也不会卡顿——因为主线程没有“空闲时间被抢占”。

2. 未触发“紧急更新与非紧急更新的并发冲突”

startTransition 的关键价值是「在非紧急更新执行期间,允许紧急更新插队」。如果你的操作中,紧急更新(输入框输入)和非紧急更新(过滤列表)没有“时间重叠”,也不会有差异。

举个反例:如果过滤操作需要 500ms(比如 10 万条数据),不使用 startTransition 时,你在输入“a”后,输入框会卡住 500ms 才显示“a”(因为主线程在处理过滤,无法响应输入);而使用 startTransition 时,输入框会立即显示“a”,过滤操作在后台继续,卡顿感消失。

但如果你的过滤操作很快(比如 1ms),两种方式下输入框都会立即响应,自然无差异。

3. 浏览器渲染优化“掩盖”了差异

现代浏览器(Chrome、Edge 等)有「主线程调度优化」,即使非紧急更新未标记 startTransition,如果计算量不大,浏览器也会尽量拆分任务,避免阻塞紧急更新。这种情况下,有无 startTransition 的最终体验差异被缩小。

4. 未关注“微交互细节”(如输入框光标/响应速度)

有时差异不是“卡顿与否”,而是“响应的细腻度”。比如数据量中等(1 万条)时,不使用 startTransition 可能导致输入框光标“轻微抖动”(主线程偶尔阻塞),而使用后光标完全流畅——但这种细微差异容易被忽略,尤其在开发环境中注意力集中在逻辑而非交互细节时。

二、如何复现 startTransition 的真实效果?制造“性能瓶颈场景”

要看到明显差异,需要 人为制造“非紧急更新阻塞主线程”的场景,让 startTransition 的“优先级调度”能力发挥作用。以下是具体可操作的方案:

步骤 1:放大数据量,让过滤操作耗时超过 16ms

items 列表扩容到 10 万条以上(模拟真实项目中的大数据场景),此时 filter 操作会占用主线程更长时间,触发阻塞:

jsx 复制代码
// 父组件中生成 10 万条模拟数据(而非之前的 1 万条)
const generateItems = () => {
  const categories = ["技术", "产品", "设计", "市场", "运营", "数据"];
  const items = [];
  // 改为 10 万条数据
  for (let i = 0; i < 100000; i++) {
    const randomCategory = categories[Math.floor(Math.random() * categories.length)];
    items.push(`${randomCategory} - 项目 ${i + 1}`);
  }
  return items;
};

步骤 2:添加“复杂计算”,进一步延长非紧急更新耗时

除了 filter,再加入“字符串处理”“正则匹配”等复杂逻辑,模拟真实场景中更重的计算(比如搜索时需要匹配多个字段):

jsx 复制代码
startTransition(() => {
  const lowerValue = value.toLowerCase();
  // 增加复杂计算:匹配多个条件 + 字符串切割
  const result = items.filter((item) => {
    const [category, name] = item.split(" - "); // 切割字符串
    // 同时匹配分类和项目名,增加计算量
    return category.toLowerCase().includes(lowerValue) || 
           name.toLowerCase().includes(lowerValue);
  });
  setFiltered(result);
});

步骤 3:对比两种模式的操作体验

此时分别测试“使用 startTransition”和“不使用 startTransition”的效果:

对比维度 不使用 startTransition 使用 startTransition
输入框响应速度 输入字符后,光标卡顿 200-500ms 才显示 输入字符后,光标立即显示,无卡顿
页面交互流畅度 输入时无法滚动页面/点击其他按钮 输入时可正常滚动/点击,操作不阻塞
加载状态提示 可能先卡顿,再显示“Filtering...” 立即显示“Filtering...”,无卡顿

三、补充:startTransitionuseDeferredValue 的区别(避免混淆)

有时会误以为 startTransition 没效果,是因为和 useDeferredValue 混淆了——两者都是“优先级调度工具”,但适用场景不同:

工具 核心作用 适用场景
startTransition 标记“非紧急更新函数”,函数内的 setState 低优先级 主动触发的更新(如搜索过滤、列表排序)
useDeferredValue 生成“延迟更新的值”,值变化时低优先级更新 被动依赖的值(如根据过滤结果渲染子组件)

如果你的场景更适合 useDeferredValue(比如过滤结果需要传给子组件),用 startTransition 也可能感觉效果不明显。例如:

jsx 复制代码
// 适合 useDeferredValue 的场景
const deferredFiltered = useDeferredValue(filtered);
// 子组件使用延迟后的值,避免频繁重渲染
return <ResultList data={deferredFiltered} />;

总结

startTransition 不是“万能优化工具”,而是“针对性解决主线程阻塞”的方案。只有当 非紧急更新耗时超过 16ms、且与紧急更新并发执行 时,其效果才会明显。如果你的项目中数据量小、计算逻辑简单,感受不到差异是正常的——这说明当前场景不需要额外的优先级调度,原生性能已足够。

在实际开发中,建议结合「真实数据量」和「用户交互场景」判断是否需要使用:比如管理系统的“大数据表格搜索”“百万级日志筛选”等场景,startTransition 能显著提升体验;而个人博客的“几十条文章搜索”,则完全不需要。

💡 写在最后

React 的并发特性正在不断进化,从 useTransitionuseDeferredValue,再到 React 19 的 useOptimistic它们的目标都是让 UI 更“像人”一样:有优先级、有耐心、有反馈。


是否希望我帮你在这篇博客末尾,附上一个 “React 并发特性家族图(useTransition、useDeferredValue、useOptimistic 之间关系图)”
可以让你的文章更直观、有图有真相。

评论

评论

发表评论