资讯动态

TanStack Start 增量静态再生(ISR)完全指南:基于 Cache-Control 的按需重验证与多级缓存策略

发布时间:2026/9/15 14:35:47 来源:尧图企业网站定制
TanStack Start 增量静态再生ISR完全指南基于 Cache-Control 的按需重验证与多级缓存策略【免费下载链接】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 StartReact Router 之上构建的客户端优先、服务端能力完备的全栈框架如何实现增量静态再生ISR。与框架私有的 ISR 实现不同TanStack Start 完全基于标准 HTTP 缓存头Cache-Control、ETag 等与任意 CDN 协作让你在页面级与数据级都拥有完全的控制权。读完本文你将掌握构建时静态预渲染与 CDN 缓存配合的基础配置、max-age/s-maxage/stale-while-revalidate等指令的精确语义、基于 Server Functions 与中间件的缓存头注入、按需重验证On-Demand Revalidation的完整实现以及面向博客、电商、营销页、用户中心等真实场景的多级缓存组合方案。ISR 是什么以及 TanStack Start 为何选择 HTTP 缓存头Incremental Static Regeneration增量静态再生ISR的核心价值在于从 CDN 提供静态生成的内容同时在后台定期重新生成。这样既拥有静态站点的性能优势CDN 边缘节点直接返回 HTML又保留了动态内容的新鲜度。TanStack Start 的实现思路与框架绑定型 ISR 截然不同它完全依赖任何 CDN 都支持的标准 HTTP 缓存头。这一设计的直接收益是不依赖特定云厂商的私有 API 即可实现 ISR 主体能力缓存行为可以在页面级与数据级分别精确控制迁移部署目标时无需改动应用代码只需调整缓存策略。其核心概念可以归纳为四步静态预渲染Static Prerendering页面在构建阶段生成 HTMLCDN 缓存CDN Caching通过缓存头控制 CDN 缓存 HTML 的时长重新验证Revalidation缓存过期后下一次请求触发重新生成Stale-While-Revalidate过期期间重新验证后台拉取新数据的同时向用户继续返回过期内容。缓存头策略基于时间的重新验证最经典的 ISR 模式是在路由配置中同时声明预渲染路由与Cache-Control 头前者解决“首屏立即有内容”后者解决“内容定期刷新”。第一步构建时预渲染路由以下配置来自 TanStack Start 官方指南。Vite 与 Rsbuild 两种打包器的写法对比如下import { tanstackStart } from tanstack/react-start/plugin/vite import { defineConfig } from vite export default defineConfig({ plugins: [ tanstackStart({ prerender: { routes: [/blog, /blog/posts/*], crawlLinks: true, }, }), ], })import { defineConfig } from rsbuild/core import { pluginReact } from rsbuild/plugin-react import { tanstackStart } from tanstack/react-start/plugin/rsbuild export default defineConfig({ plugins: [ pluginReact(), tanstackStart({ prerender: { routes: [/blog, /blog/posts/*], crawlLinks: true, }, }), ], })这里routes明确列出需要预渲染的路径支持*通配符crawlLinks: true表示从已生成的 HTML 中继续提取链接并递归预渲染。关于预渲染的完整参数enabled、autoSubfolderIndex、concurrency、filter、retryCount、failOnError等可参考 静态预渲染指南其底层实现在 packages/start-plugin-core/src/prerender.ts例如并发数默认取os.cpus().lengthprerender.ts#L93crawlLinks默认开启prerender.ts#L222失败重试与failOnError逻辑也集中于此prerender.ts#L229-L237。第二步在路由上声明缓存头预渲染只解决“生成”问题缓存时长完全由路由的headers配置决定// routes/blog/posts/$postId.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/blog/posts/$postId)({ loader: async ({ params }) { const post await fetchPost(params.postId) return { post } }, headers: () ({ // Cache at CDN for 1 hour, allow stale content for up to 1 day Cache-Control: public, max-age3600, s-maxage3600, stale-while-revalidate86400, }), }) export default function BlogPost() { const { post } Route.useLoaderData() return ( article h1{post.title}/h1 div{post.content}/div /article ) }headers是一个函数返回将写入响应的 HTTP 头。缓存过期后下一次请求会触发后台重新生成——这正是 ISR 的“增量”所在。理解 Cache-Control 指令public响应可被任意缓存CDN、浏览器等缓存max-age3600内容在 3600 秒1 小时内保持新鲜s-maxage3600对共享缓存CDN覆盖max-age的值stale-while-revalidate86400在后台重新验证期间最长 24 小时内可继续向用户返回过期内容immutable内容永不变化适用于带哈希指纹的静态资源。ISR 与 Server Functions数据端点级缓存页面级缓存之外动态数据端点同样可以设置缓存头。TanStack Start 的 Server Functions / Server Routes 允许在路由的server.handlers中直接构造Response并附加缓存头// routes/api/products/$productId.ts import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/api/products/$productId)({ server: { handlers: { GET: async ({ params, request }) { const product await db.products.findById(params.productId) return Response.json( { product }, { headers: { Cache-Control: public, max-age300, stale-while-revalidate600, CDN-Cache-Control: max-age3600, // Cloudflare-specific }, }, ) }, }, }, })注意这里两个头的分工Cache-Control控制常规缓存语义CDN-Cache-Control是 Cloudflare 特有的、用于更精细控制边缘缓存的扩展头。使用中间件统一注入缓存头当多个 API 路由需要相同缓存策略时可以使用 中间件 集中处理避免在每个 handler 里重复写头// routes/api/products/$productId.ts import { createFileRoute } from tanstack/react-router import { createMiddleware } from tanstack/react-start const cacheMiddleware createMiddleware().server(async ({ next }) { const result await next() // Add cache headers to the response result.response.headers.set( Cache-Control, public, max-age3600, stale-while-revalidate86400, ) return result }) export const Route createFileRoute(/api/products/$productId)({ server: { middleware: [cacheMiddleware], handlers: { GET: async ({ params }) { const product await db.products.findById(params.productId) return Response.json({ product }) }, }, }, })中间件在 handler 执行前后都有机会介入这里先调用next()拿到响应再向result.response.headers写入缓存头。中间件的请求/响应定制能力参见 中间件指南。对于页面路由则更推荐直接在headers属性中声明写法更简洁// routes/blog/posts/$postId.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/blog/posts/$postId)({ loader: async ({ params }) { const post await fetchPost(params.postId) return { post } }, headers: () ({ Cache-Control: public, max-age3600, stale-while-revalidate86400, }), })按需重新验证On-Demand Revalidation基于时间的重新验证适用于大部分场景但当内容被主动更新如编辑发布了一篇文章时你可能希望立即失效特定页面而不是等待缓存自然过期。此时可以通过一个 Server Route 暴露按需失效端点调用 CDN 的缓存清除 API// routes/api/revalidate.ts import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/api/revalidate)({ server: { handlers: { POST: async ({ request }) { const { path, secret } await request.json() // Verify secret token if (secret ! process.env.REVALIDATE_SECRET) { return Response.json({ error: Invalid token }, { status: 401 }) } // Trigger CDN purge via your CDNs API await fetch( https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache, { method: POST, headers: { Authorization: Bearer ${CF_API_TOKEN}, Content-Type: application/json, }, body: JSON.stringify({ files: [https://yoursite.com${path}], }), }, ) return Response.json({ revalidated: true }) }, }, }, })实现要点通过secret与process.env.REVALIDATE_SECRET比对做鉴权防止开放端点被滥用按需失效是与 CDN 强相关的——示例调用的是 Cloudflare Purge Cache APINetlify、Vercel 等平台有各自的 purge API实现时需替换为对应平台的调用失效后下一次请求将重新生成页面重新进入缓存周期。CDN 特定配置不同 CDN 对缓存头的支持细节略有差异以下是 TanStack Start 官方指南给出的三家主流平台配置。Cloudflare WorkersCloudflare 尊重标准Cache-Control并提供额外的CDN-Cache-Control头做更细粒度控制export const Route createFileRoute(/products/$id)({ headers: () ({ Cache-Control: public, max-age3600, // Cloudflare-specific header for finer control CDN-Cache-Control: max-age7200, }), })NetlifyNetlify 使用Cache-Control头同时支持_headers文件对路径批量声明缓存规则# public/_headers /blog/* Cache-Control: public, max-age3600, stale-while-revalidate86400 /api/* Cache-Control: public, max-age300Vercel部署到 Vercel 时可使用其 Edge Network 缓存头export const Route createFileRoute(/posts/$id)({ headers: () ({ Cache-Control: public, s-maxage3600, stale-while-revalidate86400, }), })更多部署配置可参考 托管指南。将 ISR 与客户端缓存结合多级缓存策略CDN 缓存只是第一层。TanStack Router 内置的客户端缓存控制可以与之并行工作形成多级缓存export const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) { return fetchPost(params.postId) }, // CDN caching (via headers) headers: () ({ Cache-Control: public, max-age3600, stale-while-revalidate86400, }), // Client-side caching (via TanStack Router) staleTime: 60_000, // Consider data fresh for 60 seconds on client gcTime: 5 * 60_000, // Keep in memory for 5 minutes })路由级staleTime与gcTime是 TanStack Router 数据加载层的内置选项其类型声明位于 packages/router-core/src/route.ts#L1279-L1280 的UpdatableRouteOptions中客户端加载与过期回收的具体逻辑可参考 packages/router-core/src/load-client.ts 与 packages/router-core/src/router.ts。详细语义参见 数据加载指南。上述配置构建出的两级缓存策略CDN Edge缓存 1 小时stale-while-revalidate允许 24 小时内继续服务过期内容并后台刷新客户端60 秒内视为新鲜数据直接复用内存中保留 5 分钟。常见 ISR 模式速查博客文章内容更新频率低缓存时间适中export const Route createFileRoute(/blog/$slug)({ loader: async ({ params }) fetchPost(params.slug), headers: () ({ // Cache for 1 hour, allow stale for 7 days Cache-Control: public, max-age3600, stale-while-revalidate604800, }), staleTime: 5 * 60_000, // 5 minutes client-side })电商商品页库存、价格变化快缩短缓存export const Route createFileRoute(/products/$id)({ loader: async ({ params }) fetchProduct(params.id), headers: () ({ // Shorter cache due to inventory changes Cache-Control: public, max-age300, stale-while-revalidate3600, }), staleTime: 30_000, // 30 seconds client-side })营销落地页内容稳定可长时间缓存export const Route createFileRoute(/landing/$campaign)({ loader: async ({ params }) fetchCampaign(params.campaign), headers: () ({ // Long cache for stable content Cache-Control: public, max-age86400, stale-while-revalidate604800, }), staleTime: 60 * 60_000, // 1 hour client-side })用户专属页面包含私有数据禁止 CDN 缓存仅浏览器私有缓存export const Route createFileRoute(/dashboard)({ loader: async () fetchUserData(), headers: () ({ // Private cache, no CDN caching Cache-Control: private, max-age60, }), staleTime: 30_000, })最佳实践1. 从小处着手逐步放宽先使用较短的缓存时间摸清内容更新规律后再逐步延长// Start here Cache-Control: public, max-age300, stale-while-revalidate600 // Then move to Cache-Control: public, max-age3600, stale-while-revalidate864002. 使用 ETag 做条件验证ETag 帮助 CDN 高效地重新验证内容避免全量传输。可通过中间件基于响应内容计算哈希生成import { createMiddleware } from tanstack/react-start import crypto from crypto const etagMiddleware createMiddleware().server(async ({ next }) { const result await next() // Generate ETag from response content const etag crypto .createHash(md5) .update(JSON.stringify(result.data)) .digest(hex) result.response.headers.set(ETag, ${etag}) return result })3. 按查询参数区分缓存键当内容随查询参数变化时将相关头纳入缓存键export const Route createFileRoute(/search)({ headers: () ({ Cache-Control: public, max-age300, Vary: Accept, Accept-Encoding, }), })4. 监控缓存命中率通过中间件读取 CDN 返回的缓存状态头持续跟踪性能const cacheMonitoringMiddleware createMiddleware().server( async ({ next }) { const result await next() // Log cache status (from CDN headers) console.log(Cache Status:, result.response.headers.get(cf-cache-status)) return result }, )5. 与静态预渲染组合构建时预渲染保证首屏秒开ISR 负责后续更新import { tanstackStart } from tanstack/react-start/plugin/vite import { defineConfig } from vite export default defineConfig({ plugins: [ tanstackStart({ prerender: { routes: [/blog, /blog/posts/*], crawlLinks: true, }, }), ], })import { defineConfig } from rsbuild/core import { pluginReact } from rsbuild/plugin-react import { tanstackStart } from tanstack/react-start/plugin/rsbuild export default defineConfig({ plugins: [ pluginReact(), tanstackStart({ prerender: { routes: [/blog, /blog/posts/*], crawlLinks: true, }, }), ], })调试 ISR检查缓存头使用curl或浏览器 DevTools 检查响应头确认缓存是否生效curl -I https://yoursite.com/blog/my-post # Look for: # Cache-Control: public, max-age3600, stale-while-revalidate86400 # Age: 1234 (time in cache) # X-Cache: HIT (from CDN)Age表示内容已在缓存中驻留的秒数X-Cache: HIT表明请求由 CDN 直接命中返回。测试重新验证强制缓存未命中以验证重新生成流程# Cloudflare: Bypass cache curl -H Cache-Control: no-cache https://yoursite.com/page # Or use CDN-specific cache purge APIs监控性能指标缓存命中率Cache Hit Rate从缓存直接返回的请求占比重新验证时间Revalidation Time重新生成过期内容所耗时长首字节时间TTFB缓存命中时应保持在较低水平。延伸阅读静态预渲染构建期页面生成ISR 的第一环托管部署各 CDN 平台的部署配置Server Functions创建动态数据端点数据加载客户端缓存控制staleTime、gcTime中间件请求/响应自定义与统一缓存头注入【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价