TanStack Start 的 React 客户端入口与延迟水合边界:react-start-client 包实战解析
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
@tanstack/react-start-client是 TanStack Start(本项目 router 仓库中 "client-first, server-capable" 全栈框架)的 React 客户端侧核心包,负责在浏览器端引导路由器完成水合(hydration),并承载一套可延迟、可预取、可代码分割的 Hydrate 边界体系。本文以该包 CHANGELOG.md 中记录的版本演进为线索,结合仓库源码,讲清这个包做了什么、Hydrate 边界如何工作、1.168.0 的延迟水合里程碑改了什么,以及子路径导出重构背后的打包体积考量。
从 CHANGELOG 读懂一个包的定位
打开 packages/react-start-client/CHANGELOG.md,大部分条目是常规的 Patch Changes(依赖升级),但其中有几条带有实质技术说明的 Minor/Patch 变更,恰好勾勒出这个包的核心职责:
- 1.168.0(Minor):为 TanStack Start 增加延迟 Hydrate 边界支持(PR #7362)——Hydrate 边界可被 Start 编译器代码分割、预取生成的客户端 chunk、保留服务端渲染的 fallback HTML,并在水合后回放交互触发的事件。
- 1.168.5(Patch):避免把客户端水合入口拖进根级
@tanstack/react-start/@tanstack/solid-start的导入,改为从框架客户端的 Hydrate-only 子路径重导出Hydrate(PR #7492)。 - 1.166.17(Patch):用自研实现替换
tiny-invariant和tiny-warning,进一步压缩打包体积(PR #7007)。 - 1.166.11(Patch):构建工具链升级到 vite-config 5.x(基于 rolldown,PR #6926)。
也就是说,这个包是"客户端引导 + 水合边界运行时 + 构建期代码分割"三个能力面的交汇点。从 package.json 可以确认它的依赖关系:@tanstack/react-router、@tanstack/router-core、@tanstack/start-client-core均为 workspace 依赖——客户端渲染层由 react-router 提供,跨框架的框架无关核心(gate、策略、运行时工具)沉淀在 start-client-core,本包只做 React 侧的薄封装。
客户端入口:StartClient 与 hydrateStart
包的主入口 src/index.tsx 只有两行导出:
'use client' export { StartClient } from './StartClient' export { hydrateStart } from './hydrateStart'StartClient(src/StartClient.tsx)是整个 React 应用的客户端挂载点:首次渲染时把hydrateStart()的 Promise 缓存为单例,然后用 react-router 的Await+RouterProvider在 Promise 完成后挂载路由器:
let hydrationPromise: Promise<AnyRouter> | undefined export function StartClient() { if (!hydrationPromise) { hydrationPromise = hydrateStart() } return ( <Await promise={hydrationPromise} children={(router) => <RouterProvider router={router} />} /> ) }hydrateStart(src/hydrateStart.ts)是对 start-client-core 同名函数的 React 包装,唯一多做的事是在水合完成后回调全局钩子window.$_TSR?.h(),向框架信号"水合完成":
export function hydrateStart(): Promise<AnyRouter> { return coreHydrateStart().finally(() => window.$_TSR?.h()) }从源码结构看,window.$_TSR是 TanStack Start 注入浏览器的全局运行时对象,.h()即 hydration 完成信号——这是服务端水合管线与客户端交互的关键握手点。
Hydrate 边界:组件与策略体系
包内最核心的组件是Hydrate(src/Hydrate.tsx),其 props 类型设计为可辨识联合:
type HydrateCommonOptions = { when: HydrateWhen // 水合策略:HydrationStrategy 或返回策略的函数 fallback?: React.ReactNode // 水合完成前展示的 fallback onHydrated?: () => void // 水合完成回调 } export type HydrateOptions = | (HydrateCommonOptions & { prefetch?: never; split?: boolean }) | (HydrateCommonOptions & { prefetch: HydrationPrefetchStrategy; split?: true }) | (HydrateCommonOptions & { prefetch: HydrationPrefetchFunction; split?: boolean })when支持传入策略对象或返回策略的函数(后者对应"动态 Hydrate",服务端渲染时走ServerDynamicHydrate,客户端再按实际策略渲染)。渲染出的 marker 元素带有两个数据属性data-ts-hydrate-id与data-ts-hydrate-when,供服务端标记与编译器识别。
水合策略共有 6 种,全部从@tanstack/start-client-core/hydration的框架无关核心中派生,React 侧只负责绑定渲染器(withHydrationRenderer绑定GenericHydrate):
| 策略 | 导出位置(React 侧) | 触发时机 | 关键默认值(见 start-client-core 源码) |
|---|---|---|---|
load() | src/hydration/load.tsx | 立即水合,渲染时同步解析 | 无延迟,走LoadHydrate(Suspense + HydratedBoundary) |
idle(options?) | src/hydration/idle.ts | 浏览器空闲时水合 | timeout默认 2000ms;优先requestIdleCallback,否则回退setTimeout |
visible(options?) | src/hydration/visible.ts | 元素进入视口时水合 | rootMargin默认'600px',threshold默认0,基于 IntersectionObserver 并按参数组合缓存 observer |
never() | src/hydration/never.tsx | 永不客户端水合 | 客户端渲染时直接替换为 fallback |
condition(fn)/interaction(options?)/media(query) | src/hydration/generic.ts | 按条件函数 / 交互事件(如 click、focus)/ CSS 媒体查询 | interaction默认监听若干交互事件;media绑定媒体查询匹配结果 |
以idle为例,核心实现在 packages/start-client-core/src/hydration/idle.ts:优先requestIdleCallback(callback, { timeout }),不支持时退化为setTimeout(callback, timeout),并在卸载时取消调度。visible的实现则在 packages/start-client-core/src/hydration/visible.ts:用rootMargin|threshold作为 key 复用共享的 IntersectionObserver,元素进入视口即触发回调并unobserve。
在 src/GenericHydrate.tsx 中可以看到这套机制的运行时骨架:每个 Hydrate 边界对应一个 gate(水合闸门),useHydrationGate负责解析策略、启动预取(prefetch支持函数式与声明式两种,含 AbortController 取消)、注册"委托式水合意图"监听;HydrationGate在客户端借助reactUse(React 的use)或直接抛 Promise 来暂停子树渲染,直到 gate 解析;HydratedBoundary在水合后调用onHydrated并移除data-ts-hydrate-when标记。值得注意的细节是:当shouldPreserveServerHTML为真时,fallback 会优先使用getFallbackHtml取回的服务端渲染原始 HTML(dangerouslySetInnerHTML+display: contents),而不是重新渲染 fallback——这正是"保留服务端渲染的 fallback HTML"这一能力的客户端落地。
1.168.0 里程碑:可代码分割的延迟 Hydrate 边界
CHANGELOG 中 1.168.0 的 Minor Changes 是本包最重要的功能变更,原文要点如下(PR #7362):
Hydrate boundaries can now be code-split by the Start compiler, preload their generated client chunks, preserve server-rendered fallback HTML, and replay interaction-triggered events after hydration. The compiler integration now uses a Start-owned compiler plugin for Hydrate virtual modules across Vite and Rsbuild, with dev invalidation for generated virtual modules.
逐条拆解这意味着什么:
- Hydrate 边界可被代码分割:之前 Hydrate 边界内的代码会随主 bundle 一起下发;1.168.0 之后,Start 编译器把边界内的路由/组件模块切成独立的客户端 chunk,只有边界真正进入水合阶段才加载执行。
- 预取生成的客户端 chunk:结合上面
prefetch策略与visible(默认 600px 视口提前量)、idle等策略,编译器生成 chunk 的预取时机可被水合策略驱动,实现"临近触发时先下载、触发时才水合"的渐进式体验。 - 保留服务端渲染的 fallback HTML:水合发生前,用户看到的是服务端渲染的原始 HTML(即
getFallbackHtml路径),避免闪白或布局抖动。 - 回放交互触发的事件:对于
interaction策略,若用户在水合完成前就点击了边界区域,事件会被记录并在水合后回放,保证交互不丢失。 - 编译器集成改为 Start 自有编译器插件:Hydrate 虚拟模块(virtual modules)的生成从路由器侧转移到 Start 侧插件,且同时覆盖 Vite 与 Rsbuild;开发模式下对生成的虚拟模块提供失效机制(dev invalidation),保证 HMR 与增量构建的正确性。
配套的架构调整是:路由器代码分割器与 Hydrate 虚拟模块共用的 AST 工具被上移到@tanstack/router-utils,两条管线(router code-splitter 与 Hydrate 虚拟模块)都能借此"保留被引用的顶层声明、解包本地导出、让死代码消除移除未使用的路由模块代码"。这正是 packages/router-utils 存在的意义之一。
1.168.5:子路径导出重构,防止入口污染
1.168.5 的 Patch 变更(PR #7492)描述如下:
Avoid pulling the client hydration entry into root
@tanstack/react-startand@tanstack/solid-startimports by re-exportingHydratefrom framework client Hydrate-only subpaths.
含义是:此前从框架根包导入Hydrate会连带引入整个客户端水合入口(hydrateStart、StartClient等),对仅做 SSR 的框架根导入造成不必要的体积污染。修复方式是让Hydrate改从客户端包的 Hydrate-only 子路径重导出。对应到本包的 package.json 的exports字段:
"exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" } }, "./hydration": { "import": { "types": "./dist/esm/hydration.d.ts", "default": "./dist/esm/hydration.js" } }, "./Hydrate": { "import": { "types": "./dist/esm/Hydrate.d.ts", "default": "./dist/esm/Hydrate.js" } }, "./package.json": "./package.json" }./Hydrate子路径只暴露 src/Hydrate.tsx(含策略类型),./hydration子路径暴露 src/hydration.ts 中的load/idle/visible/never/condition/interaction/media策略工厂。两者都不会触发主入口中StartClient/hydrateStart的加载,配合"sideEffects": false,打包器可以放心地对未使用模块做 tree-shaking。这也是 1.166.17 用自研实现替换tiny-invariant/tiny-warning(PR #7007)的同类动机——在客户端包层面持续压减基线体积。
版本节奏与依赖管理
从 CHANGELOG 的整体形态可以看出项目的发布工程化方式:
- 每个版本都对应一次 changesets 记录,Patch 居多,
1.168.0、1.167.0这类带功能的版本才升级 Minor; - 绝大多数 Patch Changes 只更新依赖(
@tanstack/react-router、@tanstack/router-core、@tanstack/start-client-core),版本号严格联动(例如 1.168.33 对应 react-router 1.170.35、router-core 1.171.29、start-client-core 1.170.29),说明三个核心包共享同一发布流水线,跨包 API 保持一致; - 少数 Patch 携带真实功能修复(如 1.168.5 子路径导出、1.166.17 依赖替换、1.166.11 构建工具升级),会在条目中附 PR 编号与 commit hash 便于回溯。
仓库根目录的 package.json(workspace 配置)与 pnpm-workspace.yaml 是这套多包协同的基础,本包通过workspace:*依赖保证与同仓版本精确对齐。
如何验证与深入阅读
本包的测试与类型校验提供了验证上述机制最直接的入口:
- packages/react-start-client/src/tests/Hydrate.test.tsx:Hydrate 边界组件的行为测试;
- packages/react-start-client/src/tests/Hydrate.test-d.tsx:
HydrateOptions可辨识联合的类型级测试(如prefetch与split的合法组合); - packages/react-start-client/src/tests/hydrateStart.test.ts:
hydrateStart与window.$_TSR信号的测试; - packages/react-start-client/src/tests/createServerFn.test-d.tsx:与 Server Function 相关的类型契约测试。
package.json 中的脚本test:types会跨 TypeScript 5.6~7.0 多个版本做类型检查(ts56 到 ts70),可见该包对类型兼容性的要求很高。
若要观察延迟水合与代码分割的完整工程形态,仓库中还有两个直接相关的示例/基准场景:e2e/react-start/deferred-hydration(延迟水合端到端场景)与 e2e/react-start/rsc-deferred-hydration(RSC 与延迟水合结合的场景),以及在 benchmarks/ssr/scenarios 中对应的渲染性能基准。
小结
@tanstack/react-start-client的角色可以概括为三层:入口层(StartClient+hydrateStart,负责浏览器端路由器引导与水合信号)、运行时层(Hydrate边界 + 6 种水合策略 + gate/预取/HTML 保留机制)、构建层契约(Hydrate 虚拟模块、代码分割、./Hydrate与./hydration子路径导出)。CHANGELOG 中 1.168.0 的延迟 Hydrate 边界支持与 1.168.5 的子路径重构,分别代表了这条主线的两个方向:让边界"更晚、更省地水合",以及让边界代码"按需、不污染地导入"。阅读本包源码时,建议按index → StartClient/hydrateStart → Hydrate → GenericHydrate → hydration/(各策略)→ start-client-core/hydration的顺序推进,即可完整串起从浏览器引导到单边界水合的整条链路。
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考