TanStack Start 延迟水合(Deferred Hydration)实战指南:用Hydrate边界精准控制首屏交互时机
【免费下载链接】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
延迟水合(Deferred Hydration)目前仍处于实验阶段,API 可能在后续版本中调整。
TanStack Start 默认在首屏加载时对整份 SSR 文档执行水合(hydration),让服务端产出的 HTML 迅速可交互。但大型页面的启动时间往往浪费在加载并水合那些用户暂时用不到的区块上。本文以 docs/start/framework/react/guide/deferred-hydration.md 为核心,系统讲解如何用Hydrate边界把页面上的部分区域标记为"暂不可交互",让 SSR HTML 先展示、再按需水合,并深入react-start、react-start-client、start-client-core的源码,说明策略实现、代码分割与正确性保障的底层原理。读完本文,你将掌握七种水合策略的选型、when/split/prefetch三项决策、常见场景配方,以及编译器对可分割边界的提取限制。
为什么需要延迟水合
首次页面加载时,TanStack Start 在服务端渲染出页面 HTML,浏览器可以立刻显示有用的内容。水合(hydration)则是客户端把这份初始 HTML 文档变成可交互应用的工作:加载并执行 JavaScript、运行组件、绑定事件处理器、把现有 DOM 重新接入 React。
延迟水合只作用于这份初始文档的水合过程。应用运行之后,后续的客户端导航完全由客户端应用渲染,不存在需要保留的初始服务端 HTML,因此不涉及延迟水合。
默认情况下,TanStack Start 对整份文档水合,这通常是最简单、最安全的行为;但大型页面可能会把可观的启动时间花在加载 JavaScript 和水合用户当下并不需要的页面区块上。延迟水合允许你把页面的选定部分标记为"暂不交互":服务端 HTML 仍然保留在文档中,TanStack Start 会等到某个策略(strategy)判定时机已到才水合该边界。默认情况下,编译器还会把边界内的子元素移动到一个独立的 JavaScript chunk 中,浏览器可以推迟加载这段代码。
延迟水合适用于这样的场景:页面某部分需要立即可见、可样式化、可被索引,但不需要立即可交互。
添加一个延迟水合边界
使用从@tanstack/react-start/hydration导出的Hydrate组件与策略:
import { Hydrate } from '@tanstack/react-start' import { visible } from '@tanstack/react-start/hydration' export function ProductPage() { return ( <Hydrate when={visible({ rootMargin: '400px' })}> <Reviews /> </Hydrate> ) }在首次服务端响应中,Reviews仍然被渲染成 HTML。在初始客户端水合过程中,这段 HTML 被保留,但Reviews的 React 树暂不水合。当边界进入视口 400px 范围内时,TanStack Start 才加载被延迟的子 chunk 并水合该边界。
Hydrate只保留初始文档中已经存在的服务端 HTML。如果同一个边界在之后才首次挂载(例如客户端导航之后),此时没有可保留的服务端 HTML,它就会在客户端正常渲染。
从源码看,Hydrate组件的分发逻辑位于 packages/react-start-client/src/Hydrate.tsx:当when传入的是函数时,服务端渲染走ServerDynamicHydrate(仅输出一个带data-ts-hydrate-id与data-ts-hydrate-when属性的标记 div 包裹的Suspense),客户端则调用props.when()._h(props)交给对应策略的渲染器;当when是策略对象时直接调用props.when._h(props)。也就是说,每个策略都携带了自己的 React 渲染函数_h,边界标记元素正是通过这些 data 属性与运行时关联的。
选择要延迟的内容
正确的边界取决于你的页面、产品优先级和真实用户行为——TanStack Start 无法替你判断页面哪些部分可以安全推迟。
好的候选者通常是无需立即交互的 SSR 内容:
- 首屏之下的评论、留言、商品详情、相关内容或较长的营销区块;
- 富交互组件,如地图、图表、轮播、视频播放器、编辑器或嵌入内容;
- 由用户意图触发的面板,如筛选器、预览窗格或上下文工具;
- 只对特定媒体查询(media query)才有意义的 UI;
- 不应在初始文档水合的静态 SSR 内容。
差的候选者是用户可能立即需要的部分:
- 主导航、路由外壳(chrome)、搜索框和账户控件;
- 首屏之上的表单、加购按钮、结账操作或同意控件;
- LCP 或 hero 区域中用户可能立刻点击的交互部分;
- 必须随页面出现立即可键盘操作的无障碍关键控件;
- 其 props、context 或共享状态预期在应用启动后立即更新的组件。
务必为每个边界做测量:一个有用的边界应当减少启动 JavaScript 或水合工作量,同时不会让预期的交互显得迟滞。
与 Astro Islands 的对比
Astro 以静态为起点,问的是"哪些内容应该活过来?"每个答案都是一个被丢进 HTML 的独立框架根节点。Islands 是共享同一个 DOM 的独立运行时。
TanStack Start 则以完全可交互为起点,问的是"哪些内容可以等待?"整份文档默认作为一个 React 树水合,Hydrate边界只是这棵树内部的闸门(gate)。Context、state 与事件都正常流动,水合按父先子后的顺序进行。
两者共享同一套触发词汇,底层基底却不同:Astro 组合运行时,Start 调度单棵树。这正是 Start 能提供interaction()、condition()以及意图冒泡(intent bubbling),而 Astro 能提供多框架支持的原因。
与 React 选择性水合的对比
React 的选择性水合(selective hydration)控制的是服务端渲染边界水合的顺序;延迟水合控制的是每个边界是否水合以及何时水合。
当 React 水合一个流式 SSR 页面时,每个服务端渲染的<Suspense>边界最终都会水合。选择性水合只是决定顺序:每个边界在其代码到达后尽快水合,如果用户点击了边界内部,React 会把它跳到队列最前面。水合的总工作量由服务端渲染的内容决定,React 只是调度它以保证响应性。
延迟水合改变的则是队列里一开始放什么。一个Hydrate边界声明一个条件——visible()、idle()、interaction()、media()、condition()或never()——在条件触发前,边界保持为静态服务端 HTML。默认情况下,子 JavaScript 也会被移入独立的 chunk,浏览器直到边界即将水合时才下载它。如果条件永不触发,边界就永不水合,其代码也永不被拉取。
两者可以组合使用:Hydrate边界决定 React 是否、以及何时开始水合某个子树;一旦闸门打开,其内部的一切(包括<Suspense>边界)都会回到 React 正常的水合调度器中。需要水合且希望 React 做好优先级时用<Suspense>;水合可能根本不需要发生时用Hydrate。
每个边界的三个性能决策
每个Hydrate边界都有三个独立的性能决策:
| 决策 | 选项 | 控制内容 |
|---|---|---|
| 水合时机(Hydration) | when | 保留的服务端 HTML 何时变得可交互。 |
| 代码分割(Code split) | split | 子元素是否移入生成的延迟子 chunk。 |
| 预准备(Preparation) | prefetch | 是否在when策略水合子内容之前就开始准备工作。 |
when:决定边界何时水合
when是必填项。常见场景直接传入策略对象:
<Hydrate when={visible()}> <Reviews /> </Hydrate>需要依赖浏览器专属信息时,传入函数:
import { Hydrate } from '@tanstack/react-start' import { interaction, visible } from '@tanstack/react-start/hydration' export function RecommendationsBoundary() { return ( <Hydrate when={() => navigator.connection?.saveData ? interaction({ events: 'click' }) : visible() } > <Recommendations /> </Hydrate> ) }函数形式只在客户端求值,且必须同步返回一个策略。当你刻意想让初始服务端 HTML 保持静态时,使用never()。
从 packages/react-start-client/src/Hydrate.tsx 可以看到函数形式在服务端会被替换为ServerDynamicHydrate,因此服务端始终输出静态 HTML;客户端才真正求值函数并选择策略,这正是它"仅在客户端可用"的原因。
split:决定是否创建独立子 chunk
默认情况下,Hydrate会把子元素拆进一个生成的子 chunk:
<Hydrate when={visible()}> <HeavyWidget /> </Hydrate>这样同时推迟了水合工作和子 JavaScript 的加载。
当子代码很小、或已经在别处被需要,你只想推迟水合工作时,设置split={false}:
import { Hydrate } from '@tanstack/react-start' import { idle } from '@tanstack/react-start/hydration' export function SmallWidgetBoundary() { return ( <Hydrate when={idle()} split={false}> <SmallWidget /> </Hydrate> ) }注意split必须是字面量false,不能使用split={shouldSplit}这类动态值——编译器需要在编译期就确定能否做静态提取(详见后文"提取限制")。
prefetch:决定是否在水合前就开始加载
prefetch在边界水合之前就开始加载。它有两种形式:
| 形式 | 示例 | 适用场景 |
|---|---|---|
| 策略形式(Prefetch strategy) | prefetch={idle()} | 在水合前预加载生成的子 chunk。 |
| 程序化形式(Procedural prefetch) | prefetch={async (ctx) => { ... }} | 预加载子 chunk 以及数据或其他异步资源。 |
两种形式都会提前开始工作,但不会改变边界变得可交互的时机——那仍然由when控制。
策略形式是简洁的声明式写法:
import { idle, interaction, visible } from '@tanstack/react-start/hydration' <Hydrate when={interaction()} prefetch={idle()}> <ProductRecommendations /> </Hydrate> <Hydrate when={interaction()} prefetch={visible({ rootMargin: '1200px' })} > <RelatedProducts /> </Hydrate>策略形式的prefetch会在边界水合前下载生成的子 chunk,这可能让之后的水合触发感觉更快,因为当when解析时浏览器可能已经持有该 chunk。生成的子 chunk 只在split开启时才存在,因此 TypeScript 会在split={false}时拒绝策略形式的prefetch——这一点在 packages/react-start-client/src/Hydrate.tsx 的HydrateOptions联合类型中得到了体现:prefetch: HydrationPrefetchStrategy的分支强制split?: true。
需要自定义工作时使用程序化 prefetch:
import { useQueryClient } from '@tanstack/react-query' import { Hydrate } from '@tanstack/react-start' import { visible } from '@tanstack/react-start/hydration' function DeferredReviews() { const queryClient = useQueryClient() return ( <Hydrate when={visible()} prefetch={async ({ preload }) => { await preload() await queryClient.prefetchQuery(reviewsQueryOptions) }} > <Reviews /> </Hydrate> ) }程序化 prefetch 同样适用于split={false}。此时preload()是一个已解析的 no-op,但函数体仍然可以准备数据或其他资源。
常见配方
水合首屏之下的 SSR 内容
import { Hydrate } from '@tanstack/react-start' import { visible } from '@tanstack/react-start/hydration' export function ProductPage() { return ( <> <ProductHero /> <BuyBox /> <Hydrate when={visible({ rootMargin: '800px' })}> <Reviews /> </Hydrate> </> ) }当边界应当在真正进入视口之前就水合时,使用正的rootMargin。
在需要之前下载子 chunk
import { Hydrate } from '@tanstack/react-start' import { idle, visible } from '@tanstack/react-start/hydration' export function ReviewsBoundary() { return ( <Hydrate when={visible({ rootMargin: '200px' })} prefetch={idle()}> <Reviews /> </Hydrate> ) }这样边界在接近视口前保持不可交互,但在空闲时间就开始加载子 chunk。
让控件保持"冷"直到用户意图出现
import { Hydrate } from '@tanstack/react-start' import { interaction, visible } from '@tanstack/react-start/hydration' export function RecommendationsBoundary() { return ( <Hydrate when={interaction({ events: ['focusin', 'click'] })} prefetch={visible({ rootMargin: '1200px' })} > <RecommendationCarousel /> </Hydrate> ) }适用于可见或临近、但只有用户伸手去操作时才重要的昂贵控件。
不分割代码而仅延迟水合
import { Hydrate } from '@tanstack/react-start' import { idle } from '@tanstack/react-start/hydration' export function BadgeBoundary() { return ( <Hydrate when={idle()} split={false}> <SmallPersonalizedBadge /> </Hydrate> ) }当 JavaScript 已经在启动 bundle 中、或独立子 chunk 不值得时使用。
让初始 SSR HTML 保持静态
import { Hydrate } from '@tanstack/react-start' import { never } from '@tanstack/react-start/hydration' export function MarketingPage() { return ( <Hydrate when={never()}> <StaticTrustBadges /> </Hydrate> ) }never()保留现有服务端 HTML,且不会在初始文档水合期间水合该边界。如果同一边界在客户端导航期间稍后挂载,它会正常渲染(因为没有初始服务端 HTML 可保留)。never()不能用作 prefetch 策略。
从 packages/react-start-client/src/hydration/never.tsx 的实现看,NeverGate在客户端通过reactUse(neverPromise)抛出一个永不 resolve 的 Promise(neverPromise = new Promise<void>(() => {})),使子树永远挂起;同时用saveFallbackHtml(id, element)把边界内的服务端 HTML 保存下来、通过dangerouslySetInnerHTML原样还原,从而保证静态 HTML 不被水合逻辑破坏。
复用 Hydrate props
用HydrateOptions定义可复用的对象,再展开到Hydrate上:
import { Hydrate } from '@tanstack/react-start' import type { HydrateOptions } from '@tanstack/react-start' import { visible } from '@tanstack/react-start/hydration' const belowFoldProps = { when: () => visible({ rootMargin: '800px' }), } satisfies HydrateOptions export function Page() { return ( <Hydrate {...belowFoldProps} prefetch={async ({ preload }) => { await preload() }} > <Widget /> </Hydrate> ) }内联的when与prefetch函数是受支持的。无需用useCallback包裹它们:TanStack Start 内部始终保留最新回调,不会因为函数标识(identity)变化而重新注册水合监听器。如果边界的语义发生变化,请使用普通的 Reactkey来创建一个新边界。
Hydrate Props 参考
Hydrate接受以下 props:
| Prop | 类型 | 说明 |
|---|---|---|
when | HydrationStrategy \| () => HydrationStrategy | 必填。控制边界何时水合。函数形式仅限客户端且必须同步。 |
prefetch | HydrationPrefetchStrategy \| HydrationPrefetchFunction | 可选。策略形式预加载被分割的子 chunk。函数形式可以预加载 chunk、数据或其他资源,并可用于split={false}。 |
split | boolean | 默认为true。设置字面量false可禁用编译器提取,仅推迟水合工作。 |
fallback | ReactNode | 仅客户端使用的加载 UI,用于应用水合完成之后才挂载、并在子 chunk 或子Suspense上挂起的边界。 |
onHydrated | () => void | 边界在客户端水合完成后触发一次。 |
策略参考
策略从@tanstack/react-start/hydration导入(该模块在 packages/react-start/src/hydration.ts 中从@tanstack/react-start-client/hydration原样转发):
| 策略 | 行为 |
|---|---|
load() | 应用水合后立即水合。 |
idle() | 在requestIdleCallback中水合;空闲回调不可用时在timeout之后水合。 |
visible() | 边界标记进入视口时水合。 |
media() | 媒体查询匹配时水合。 |
interaction() | 在配置的交互意图事件上水合。 |
condition() | 条件变为真值后水合。 |
never() | 永不在初始服务端渲染的边界上水合。 |
策略选项:
| 策略 | 选项 |
|---|---|
idle | { timeout?: number },默认为2000。 |
visible | { rootMargin?: string; threshold?: number \| Array<number> },默认 margin 为600px。 |
media | 查询字符串,例如media('(min-width: 800px)')。 |
interaction | { events?: supported event or readonly array of supported events }。 |
condition | 布尔值或返回布尔值的函数。 |
支持的交互事件为:auxclick、click、contextmenu、dblclick、focusin、keydown、keyup、mousedown、mouseenter、mouseover、mouseup、pointerdown、pointerenter、pointerover、pointerup。
interaction()的默认事件列表是pointerenter、focusin、pointerdown、click——这在 packages/start-client-core/src/hydration/interaction.ts 的defaultInteractionEvents中有直接体现。当边界应当监听不同事件或更小的事件集合时使用events:
import { Hydrate } from '@tanstack/react-start' import { interaction } from '@tanstack/react-start/hydration' <Hydrate when={interaction({ events: 'dblclick' })}> <PreviewEditor /> </Hydrate> <Hydrate when={interaction({ events: ['contextmenu', 'dblclick'] })}> <ContextMenuEditor /> </Hydrate>condition()边界水合后,即使条件之后变回 false,它也会保持水合状态:
import { Hydrate } from '@tanstack/react-start' import { condition } from '@tanstack/react-start/hydration' export function CartRecommendationsBoundary() { return ( <Hydrate when={condition(isCartOpen)}> <CartRecommendations /> </Hydrate> ) }实现层面,visible()在 packages/react-start-client/src/hydration/visible.tsx 中通过IntersectionObserver观察边界标记元素,默认rootMargin: '600px'、threshold: 0,一旦isIntersecting就断开 observer 并放行;idle()则依赖requestIdleCallback,不可用时按timeout兜底。
Prefetch 参考
程序化 prefetch 接收一个上下文对象:
| 属性 | 含义 |
|---|---|
preload() | 加载编译器生成的子 chunk。split={false}时立即 resolve。 |
waitFor(strategy) | 等待一个 prefetch 策略、水合触发或中止。 |
signal | 用于可取消异步工作(如fetch)的AbortSignal。 |
element | 边界标记元素,可用于自定义观察器或 DOM 测量。 |
waitFor(strategy)的 resolve 结果(HydrationPrefetchWaitReason,定义于 packages/start-client-core/src/hydration/types.ts):
| 结果 | 含义 |
|---|---|
'prefetch' | 提供的 prefetch 策略正常 resolve。 |
'hydrate' | 边界的水合触发先发生。现在做必要的工作。 |
'abort' | 边界已卸载或 prefetch 生命周期被放弃。 |
程序化 prefetch 返回的 Promise 是有意义的。被 await 的工作会阻塞水合——如果when策略在 prefetch 函数完成之前 resolve:
<Hydrate when={visible()} prefetch={async ({ preload }) => { await preload() }} > <Widget /> </Hydrate>fire-and-forget 的工作不会阻塞水合:
<Hydrate when={visible()} prefetch={({ preload }) => { void preload() }} > <Widget /> </Hydrate>请刻意使用这种区分:当资源是首次水合渲染所必需时用 await;当资源只是有用的提前量时用 fire-and-forget。
关于 fallback 的正确理解
fallback不是初始服务端渲染 HTML 的占位符。在首次页面加载时,TanStack Start 会在边界水合前一直保留现有服务端 HTML:
<Hydrate when={visible()} fallback={<ReviewsSkeleton />}> <Reviews /> </Hydrate>这个例子里,如果Reviews存在于初始 HTML 文档中,用户看到的是服务端渲染的评论;在边界等待visible()期间不会看到ReviewsSkeleton。
fallback用于边界在应用已经运行之后才首次出现、且该边界没有现成服务端 HTML 的情况。常见场景包括客户端导航、条件性显示面板、打开一个初始文档中没有内容标签页。此时边界在客户端渲染,fallback可以在生成的子 chunk 或子Suspense仍在加载时显示。
对于never(),初始服务端 HTML 保持静态,fallback不会被使用。
编译器会从服务端 bundle 中移除静态可见的fallbackprops。请优先直接传递fallback、通过内联对象展开传递、或通过单次使用的const对象展开传递,这样服务端构建才能剥离这部分 UI。
正确性与更新
延迟水合只是对 React 初始水合工作的性能提示。如果边界外部的 state、props、context 或 store 更新要求 React 在闸门打开之前就对其内部进行调和(reconcile),React 可能早于策略允许的时机水合该延迟边界。这保证了正确性,避免在外围应用已经变化后仍显示过期的服务端 HTML。
never()是初始文档水合的例外。请把它视为刻意静态的 SSR HTML,不要指望父组件更新能让never()边界变得可交互。如果同一边界在客户端导航期间稍后挂载,它会正常渲染。
嵌套边界与意图冒泡
嵌套边界按父先子后的顺序水合。子边界只能在所有祖先边界水合之后才能水合。这意味着visible、media、idle、condition这类非交互子策略,在其父边界仍处于未水合状态时无法运行。
例如,产品页可以延迟整个评论区块直到它接近视口,同时让更重的评论工具保持"冷"直到用户与之交互:
import { Hydrate } from '@tanstack/react-start' import { interaction, visible } from '@tanstack/react-start/hydration' export function ProductPage() { return ( <> <ProductHero /> <BuyBox /> <Hydrate when={visible({ rootMargin: '600px' })}> <section aria-labelledby="reviews-heading"> <h2 id="reviews-heading">Reviews</h2> <ReviewsSummary /> <ReviewsList /> <Hydrate when={interaction({ events: ['focusin', 'click'] })}> <ReviewFilters /> </Hydrate> <Hydrate when={interaction({ events: 'click' })}> <WriteReviewForm /> </Hydrate> </section> </Hydrate> </> ) }这个例子中,滚动到评论附近先水合父边界;只有在那之后,嵌套的交互边界才能因 focus 或 click 而水合。
交互意图还可以解析未解析的祖先链——当祖先本身也在等待交互时:
<Hydrate when={interaction({ events: ['focusin', 'click'] })}> <section aria-label="Review tools"> <ReviewSortSummary /> <Hydrate when={interaction({ events: 'click' })}> <WriteReviewForm /> </Hydrate> </section> </Hydrate>如果第一个有意义的意图是WriteReviewForm内部的 click,TanStack Start 会先水合未解析的父链,然后为目标边界重新派发一个同类型事件。原生监听器 payload 的细节(如指针坐标)不保证被保留。never()祖先在初始水合期间仍然优先,因此它下面的后代保持不可交互。
预加载与 CSS 的关系
转换后的HydrateJavaScript chunk不会随路由一起被 modulepreload。没有prefetch时,子 chunk 在分割边界准备好渲染时才会加载。如果该 import 在客户端导航或其他仅客户端的挂载过程中挂起,边界的fallback会显示。
被分割、延迟以及never()边界使用的CSS 会在匹配路由的 SSR HTML 中链接。它不会随生成的子 JavaScript chunk 一起延迟,因为服务端渲染的 HTML 在任何 JavaScript 运行之前就可能需要这些样式。这是路由级的资源链接:如果路由模块包含一个导入了 CSS 的延迟边界,即使该边界藏在条件渲染后面、没有出现在某次特定响应中,该样式表也可以为该路由链接。
编译器的提取限制
编译器支持的Hydrate分割,其原理是把边界的子元素移入一个生成的虚拟模块,并通过 lazy 组件渲染它们。这让 TanStack Start 能获得一个稍后加载的独立子 chunk,但也意味着编译器必须能安全地移动 JSX。
把你要分割的组件直接放在Hydrate内部。如果把它藏在透明的childrenprops 后面,编译器无法在使用点把这些 children 静态提取进生成的子 chunk。
分割边界必须使用从@tanstack/react-start静态导入的Hydrate组件。重命名导入是受支持的:
import { Hydrate as Deferred } from '@tanstack/react-start' export function ProductPage() { return ( <Deferred when={visible()}> <Reviews /> </Deferred> ) }把Hydrate赋给另一个组件变量则不会被分析为可分割:
import { Hydrate } from '@tanstack/react-start' const Deferred = Hydrate <Deferred when={visible()}> <Reviews /> </Deferred>请直接渲染导入的Hydrate标签、使用导入重命名,或者在需要组件间接层时设置split={false}。
使用字面量 propsplit={false}来退出提取。split={shouldSplit}这类动态值无法在编译期退出。
以下模式无法被分割:
| 模式 | 拒绝原因 | 替代方案 |
|---|---|---|
| 函数作为 children(function-as-children) | 编译器无法移动渲染函数并保持预期的调用模式。 | 使用split={false}或把渲染的 UI 移入子组件。 |
| 提取的 JSX 中直接调用 Hook | 移动该 JSX 会移动 Hook 的执行位置。 | 把 Hook 调用移入边界内的组件,再渲染该组件。 |
this捕获 | 提取的函数组件无法安全保留类实例上下文。 | 用函数组件包裹 UI,或使用split={false}。 |
super捕获 | 提取的函数组件无法保留对父类的访问。 | 用函数组件包裹 UI,或使用split={false}。 |
下面这样会失败,因为useThing()会被移入生成的组件:
<Hydrate when={idle()}> <p>{useThing()}</p> </Hydrate>请把 Hook 移入组件:
function ThingText() { const thing = useThing() return <p>{thing}</p> } export function ProductPage() { return ( <Hydrate when={idle()}> <ThingText /> </Hydrate> ) }从外围组件捕获的值可以传入生成的子组件,但要保持边界简单。如果提取开始迫使数据流复杂化,优先使用具名子组件并把逻辑放进去。
fallback剥离是刻意保守的。服务端构建只能剥离直接传递的 fallback UI、内联对象展开的 fallback UI,以及单次使用const对象展开的 fallback UI。如果 fallback props 藏在动态展开或共享对象后面,编译器可能保留它们。
你今天就可以提取可复用的when与prefetch辅助函数,但如果需要子代码分割,请避免把分割边界藏在普通包装组件后面。包装组件可以在运行时延迟水合,但编译器无法可靠地通过任意组件间接层把调用点的 children 移入独立 chunk。
小结
延迟水合把"整页水合"拆解为"按需水合":服务端先输出完整可读、可索引的 HTML,客户端用Hydrate边界声明七个策略(load、idle、visible、media、interaction、condition、never)之一,控制每个区域何时、以及是否变为可交互。三个决策维度——when(何时水合)、split(是否分割子 chunk)、prefetch(是否提前准备)——让你能独立权衡启动 JS 体积、首屏交互延迟与代码加载时机;配合嵌套边界的父先子后顺序、意图冒泡、fallback语义与编译器的提取限制,即可在真实产品页面上安全落地。更多资料可参考 docs/start/framework/react/guide 下的其他指南,以及源码 packages/react-start-client/src/Hydrate.tsx、packages/react-start-client/src/hydration 与 packages/start-client-core/src/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),仅供参考