早期提示(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 可能为同一个请求多次调用 onEarlyHints。links 只包含早前阶段没有发过的值。allLinks 包含到目前为止收集的所有去重后的值。
选择何时发送提示
onEarlyHints 可以在两个阶段运行。
| 阶段 | 何时运行 | 包含什么 |
|---|---|---|
static | 路由匹配之后、路由器加载路由之前 | 匹配路由的 manifest 管理资源 |
dynamic | router.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().links 中 rel: 'stylesheet' 的条目会为了早期提示被转换成 rel=preload; as=style。这包括你用 ?url 导入并从路由 head() 返回的样式表。CSS 导入模式如何影响 Start 发现样式表的时机,见CSS 样式。
用响应 Link 头作为兜底
你也可以把收集到的提示附加到最终 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 头形式。两者都在各阶段之间去重,只包含新值,并且索引对齐。
allHints 和 allLinks 包含到目前为止为请求收集的所有去重值。它们也是索引对齐的,当你想在 dynamic 阶段写一条合并的 103 响应时很有用。
responseLinkHeader.filter 回调接收这种结构的条目:
type ResponseLinkHeaderEntry = {
phase: 'static' | 'dynamic'
hint: EarlyHint
link: string
}受支持的链接
Start 会对能干净地映射到 HTTP Link 头的 link relation 发出早期提示:
preloadmodulepreloadpreconnectdns-prefetch
当存在以下属性时,Start 会序列化它们:
hrefrelascrossOrigintypeintegrityreferrerPolicyfetchPriority
其他 head 标签、内联样式、路由脚本和元数据不会被转换成早期提示。
HTML 早期提示处理在最终文档存在之前不会应用 media、imageSrcSet 或 imageSizes,所以 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响应。