TanStack Start 中文文档
渲染

早期提示(Early Hints)

实验性

早期提示(Early Hints)是实验性功能,将来可能发生变化。

HTTP 103 Early Hints 让你的服务器在最终 HTML 响应准备好之前,提前告诉浏览器哪些是重要资源。TanStack Start 可以收集路由资源(route assets)和路由 head().links,然后调用你的服务器入口点,让你的运行时可以发送 103 响应。

Start 不会自动发送早期提示。每个部署平台暴露的写入信息性响应(informational response)的 API 都不同,所以由你的服务器入口点决定如何发送它们。

选择如何发送提示

大多数应用应该选择下面这些模式之一。

目标使用权衡
尽可能早地发送提示phase === 'static' 配合 links可能为一个最终会重定向的请求发送提示
只发送重定向安全的提示phase === 'dynamic' 配合 allLinks运行得更晚,在路由加载完成后
让 CDN 生成早期提示responseLinkHeader只对公开、缓存稳定的链接安全
支持没有 HTTP 103 的运行时responseLinkHeader 作为 preload 提示的兜底不能像 103 那样隐藏服务器思考时间

浏览器通常只处理一次导航的第一个 103 响应。每个请求最多写一个早期提示响应。

从服务器入口点发送早期提示

src/server.ts 中添加 onEarlyHints,然后把序列化后的 links 传给运行时的早期提示 API。

这个示例发送最早的静态提示:

// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request, {
      onEarlyHints: ({ phase, links }) => {
        if (phase !== 'static' || !links.length) return

        // Send `links` with your runtime-specific 103 API.
      },
    })
  },
})

Start 可能为同一个请求多次调用 onEarlyHintslinks 只包含早前阶段没有发过的值。allLinks 包含到目前为止收集的所有去重后的值。

选择何时发送提示

onEarlyHints 可以在两个阶段运行。

阶段何时运行包含什么
static路由匹配之后、路由器加载路由之前匹配路由的 manifest 管理资源
dynamicrouter.load() 完成之后(除非请求重定向)路由 head() 函数返回的支持链接;当所有提示都已发出时为一个空数组

想尽快让浏览器开始加载已知的路由资源时,使用 static。静态提示可以在路由 beforeLoad 函数之前运行,所以它们可能为一个之后会重定向的请求发送。

当提示必须是重定向安全或加载器感知时,使用 dynamic。如果你想用一条 103 响应同时包含静态路由资源和动态路由 head() 链接,就等 dynamic 阶段并发送 allLinks

onEarlyHints: ({ phase, allLinks }) => {
  if (phase !== 'dynamic' || !allLinks.length) return

  // Send one redirect-safe 103 with static and dynamic links.
  // Use `allLinks` with your runtime-specific 103 API.
}

dynamic 阶段可能以空的 links 运行,所以它也可以用作「加载完成」的信号。

从路由 Head 添加动态提示

动态早期提示来自加载器运行之后、路由 head().links 中受支持的条目。

import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => getPost(params.postId),
  head: ({ loaderData }) => ({
    links: [
      {
        rel: 'preload',
        href: loaderData.heroImageUrl,
        as: 'image',
      },
    ],
  }),
})

router.load() 产生重定向时,dynamic 阶段会被跳过。

路由 head().linksrel: 'stylesheet' 的条目会为了早期提示被转换成 rel=preload; as=style。这包括你用 ?url 导入并从路由 head() 返回的样式表。CSS 导入模式如何影响 Start 发现样式表的时机,见CSS 样式

你也可以把收集到的提示附加到最终 HTML 响应的 HTTP Link 头上。

响应 Link 头不能像 103 响应那样隐藏服务器思考时间,但浏览器在解析 HTML body 之前就能收到它,所以它仍然可以比仅从 HTML 中发现的 preload 和 preconnect 更早开始。

响应 Link 头在以下场景最有用:

  • 你的运行时无法写 103 响应。
  • 你的 CDN 能从响应 Link 头生成早期提示。

Start 不会自动添加响应 Link 头。它无法知道那些头是只被浏览器用于当前响应、被共享缓存存储,还是稍后作为 CDN 生成的早期提示被重放。

这个示例把所有收集到的静态和动态链接附加到非重定向的 HTML 响应上:

// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request, {
      responseLinkHeader: true,
    })
  },
})

在发送给 CDN 之前过滤链接

一些 CDN 可以读取响应 Link 头、缓存它们,并为之后的请求发出自己的 103 响应。例如,Cloudflare Early Hints 可以使用来自 HTML 响应的 Link 头。

只应让共享缓存或 CDN 重放那些公开且对缓存边界稳定的链接。

适合包含的好链接:

  • 静态路由的 JavaScript 和 CSS 资源。
  • URL 稳定的公开字体、图片、样式或 fetch preload。
  • 公开的 preconnect 来源。

当链接符合以下情况时,避免或过滤掉它们:

  • 需要认证、私有或用户专属。
  • 签名、过期或其他短期有效。
  • 从 cookie、请求头、查询字符串、A/B 测试或用户数据派生,除非缓存 key 也基于同样的输入变化。
  • 在你的应用授权请求之前重放不安全。

Cloudflare 记录了几个重要注意事项:它的早期提示缓存忽略查询字符串,它可以在到达你的源站或 Worker 之前发出缓存的提示,并且它只从选定的最终响应状态码和 Link 关系生成提示。

因为这些缓存语义,只有当每个发出的静态或动态链接对请求 URI 都是公开且缓存稳定时,才使用响应 Link 头。用 responseLinkHeader.filter 移除对你的缓存边界不安全的链接。

例如,只保留静态 manifest 资源:

handler.fetch(request, {
  responseLinkHeader: {
    filter: ({ phase }) => phase === 'static',
  },
})

CDN 资源重写如何影响提示

静态早期提示是从为请求解析出的最终 Start manifest 中收集的。这意味着它们遵循 transformAssets 的结果:

  • CDN URL 重写会反映在早期提示中。
  • transformAssets 返回的 crossOrigin 会反映在早期提示中。
  • JavaScript 提示遵循客户端输出格式:module 输出用 modulepreload,IIFE 输出用 preload; as=script
  • cache: false 的按请求转换会反映在该请求的早期提示中。
  • 当 Start 的 CSS inlining 构建选项把 CSS 内联进 HTML 时,内联的 CSS 资源会被跳过。

事件结构

回调接收一个 EarlyHintsEvent

type EarlyHintsEvent = {
  phase: 'static' | 'dynamic'
  hints: ReadonlyArray<EarlyHint>
  links: Array<string>
  allHints: ReadonlyArray<EarlyHint>
  allLinks: Array<string>
}

hints 是当前阶段的结构化形式。links 是当前阶段的序列化 HTTP Link 头形式。两者都在各阶段之间去重,只包含新值,并且索引对齐。

allHintsallLinks 包含到目前为止为请求收集的所有去重值。它们也是索引对齐的,当你想在 dynamic 阶段写一条合并的 103 响应时很有用。

responseLinkHeader.filter 回调接收这种结构的条目:

type ResponseLinkHeaderEntry = {
  phase: 'static' | 'dynamic'
  hint: EarlyHint
  link: string
}

受支持的链接

Start 会对能干净地映射到 HTTP Link 头的 link relation 发出早期提示:

  • preload
  • modulepreload
  • preconnect
  • dns-prefetch

当存在以下属性时,Start 会序列化它们:

  • href
  • rel
  • as
  • crossOrigin
  • type
  • integrity
  • referrerPolicy
  • fetchPriority

其他 head 标签、内联样式、路由脚本和元数据不会被转换成早期提示。

HTML 早期提示处理在最终文档存在之前不会应用 mediaimageSrcSetimageSizes,所以 Start 不会把这些属性序列化到 103 链接中。

运行时示例:Node

如果你的运行时暴露了 Node 的 ServerResponse,用 links 调用 writeEarlyHints。这个示例发送最早的 static 提示:

// src/server.ts
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
import type { ServerResponse } from 'node:http'

export default createServerEntry({
  fetch(request) {
    return handler.fetch(request, {
      onEarlyHints: ({ phase, links }) => {
        if (phase !== 'static' || !links.length) return

        const response = getNodeResponseSomehow(request) as
          | ServerResponse
          | undefined

        response?.writeEarlyHints({ link: links })
      },
    })
  },
})

getNodeResponseSomehow 替换成你的适配器暴露的 API。

运行时示例:srvx / Node 上的 Nitro

Nitro 在 Node 部署底层使用 srvx。srvx 在请求运行时上下文中暴露原生的 Node 响应。这个示例等待 dynamic 阶段,发送一条同时包含静态和动态链接的重定向安全响应:

// src/server.ts
import handler from '@tanstack/react-start/server-entry'
import type { ServerRequest } from 'srvx'

export default {
  fetch(request: Request) {
    const serverRequest = request as ServerRequest

    return handler.fetch(request, {
      onEarlyHints: ({ phase, allLinks }) => {
        if (phase !== 'dynamic') return

        const response = serverRequest.runtime?.node?.res

        if (response?.writeEarlyHints && allLinks.length) {
          response.writeEarlyHints({ link: allLinks })
        }
      },
    })
  },
}

限制

  • 早期提示在 Start 开发服务器中被跳过。
  • 只有启用 responseLinkHeader 时,Start 才会修改响应的 Link 头。
  • 浏览器通常只处理一次导航的第一个 103 响应。
  • 静态提示可能在 beforeLoad 重定向已知之前被发送。
  • 你应用前面的运行时或代理必须支持 HTTP 103 响应。

On this page