跳到主要内容

Start 服务端与部署

这一篇是 TanStack Start 真正进入项目决策层的地方。一个框架能不能落地,最后拼的不是首页 demo,而是这几件事:

  • 页面怎么渲
  • 数据和服务端逻辑怎么放
  • 运行时和部署是不是顺手
  • 出了问题你能不能定位

1. 渲染主线

官方文档给 Start 的默认叙述是:它能做 full-document SSRstreaming。这意味着它不是只在客户端补水,而是真的能在服务端先把文档级 HTML 渲出来。

默认 SSR 是什么感觉

按官方 Selective SSR 文档的描述,路由命中首个请求时:

  • beforeLoadloader 会先在服务端执行
  • 路由组件随后在服务端渲染
  • 产出的 HTML 发给浏览器
  • 浏览器再完成 hydrate

这个流程对 SEO、首屏速度和首个请求的数据直出都很重要。

一个最小 SSR 入口大致长这样

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

export default createServerEntry({
requestHandler: handler,
})

真实项目里你不一定天天改这里,但你最好知道:服务端入口不是黑盒。Start 允许你在这一层继续扩展,而不是把请求处理死锁在固定模板里。

Streaming 解决什么问题

SSR 本身不是万能药。只要某些数据慢,整个页面就可能被卡住。

官方把 streaming 当成重点能力,就是因为它允许你先把壳发出去,再把慢数据对应的部分一点点送到客户端。这个思路如果你熟悉 React Suspense,会比较容易代入。

Selective SSR 很值得注意

这是 Start 跟很多“默认整页 SSR 或整页 CSR”的方案相比,比较有意思的一点。

它允许你按路由控制:

  • 哪些路由继续走服务端执行
  • 哪些路由只保留数据层服务端处理
  • 哪些路由干脆纯客户端

这对混合型应用很实用。比如后台图表页、依赖浏览器 API 的编辑器页,就没必要强行让它们和营销页用同一种 SSR 策略。

SPA Mode 不是“放弃服务端”

官方 SPA Mode 文档里有一句很实在:不用 SSR,不等于不能继续用服务端能力。

也就是说,在 Start 里你即便把页面主体验证成 SPA:

  • 仍然可以保留 Server Functions
  • 仍然可以保留 Server Routes
  • 仍然可以做预渲染 shell

这很适合那些不太需要 SEO,但又想保留同仓服务端能力的后台系统。

一段最常见的 Start 插件配置

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'

export default defineConfig({
plugins: [
tanstackStart({
prerender: {
routes: ['/', '/docs', '/blog/posts/*'],
crawlLinks: true,
},
}),
react(),
],
})

这段配置有两个很实用的点:

  • 哪些路由要预渲染,可以显式写出来
  • crawlLinks: true 可以沿着链接继续扩展预渲染范围

2. Server Functions

如果只记一句话:

Server Functions 是 Start 最有辨识度的一层能力之一。

官方文档对它的定义很直接:你可以把“只该在服务端执行的逻辑”定义出来,然后从组件、loader、hooks 或其他 server function 里调用它;调用端和服务端之间仍保持类型约束。

一个最小示例

import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const CreatePostInput = z.object({
title: z.string().min(1),
})

export const createPost = createServerFn({ method: 'POST' })
.inputValidator(CreatePostInput)
.handler(async ({ data }) => {
return {
id: crypto.randomUUID(),
title: data.title,
}
})

组件里调用时会很像在调本地函数:

import { useState } from 'react'
import { createPost } from '../server/create-post'

export function PostForm() {
const [title, setTitle] = useState('')

async function handleSubmit() {
const result = await createPost({
data: { title },
})

console.log('created', result.id)
}

return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button onClick={handleSubmit}>创建</button>
</div>
)
}

这就是 Start 很讨人喜欢的一点:服务端逻辑离前端很近,但边界又没有糊掉。

再看一个鉴权例子

import { createServerFn } from '@tanstack/react-start'
import { redirect } from '@tanstack/react-router'

export const requireUser = createServerFn().handler(async () => {
const user = await getCurrentUser()

if (!user) {
throw redirect({ to: '/login' })
}

return user
})

这类逻辑如果全改成前端 if (!user) navigate('/login'),体验和边界都会差很多。

3. Server Routes

官方 Server Routes 文档说得很清楚:它们是处理原始 HTTP 请求的端点,适合:

  • API
  • 表单提交
  • Webhook
  • 文件/脚本型输出
  • 需要直接控制 Request / Response 的场景

它和 Server Functions 不是谁取代谁,而是关注点不同。

一个最小 GET 路由

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

export const Route = createFileRoute('/api/health')({
server: {
handlers: {
GET: async () => {
return Response.json({
ok: true,
now: new Date().toISOString(),
})
},
},
},
})

同文件既有页面也有接口

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

export const Route = createFileRoute('/contact')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
return Response.json({ received: body.email })
},
},
},
component: ContactPage,
})

function ContactPage() {
const [email, setEmail] = useState('')

async function submit() {
await fetch('/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
}

return (
<div>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button onClick={submit}>发送</button>
</div>
)
}

这种“同一路径既有 UI 又有服务端处理”的结构,在某些表单页里会很顺手。

4. 缓存、预渲染和 ISR

Start 的 ISR 文档有个我很喜欢的点:它没有把增量静态更新封成一套“只在某平台成立的神秘黑箱”,而是更偏标准 HTTP cache headers 思路。

官方文档给出的重点包括:

  • 构建期预渲染
  • CDN 缓存
  • stale-while-revalidate
  • 在 route headers 或 middleware 里显式设缓存头

路由级缓存头示例

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

export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params }) => {
const post = await fetchPost(params.slug)
return { post }
},
headers: () => ({
'Cache-Control':
'public, max-age=300, s-maxage=300, stale-while-revalidate=86400',
}),
})

用 middleware 统一补缓存头

import { createMiddleware } from '@tanstack/react-start'

export const cacheMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next()

result.response.headers.set(
'Cache-Control',
'public, max-age=600, stale-while-revalidate=3600',
)

return result
})

如果你所在团队本来就很重视 CDN 行为,这种写法会比“框架私有 revalidate 魔法”更踏实。

5. 部署

官方 Hosting 文档强调了一点:Start 建在 Vite 之上,目标是可以部署到任意 hosting provider。

文档里同时提到官方推荐的 Hosting Partners 包括:

  • Cloudflare
  • Netlify
  • Railway

也提到了 Vercel

我会先怎么选

  • 已经有 CDN / Edge 基建:先看 Cloudflare
  • 站点和函数部署都想快一点:先看 Netlify
  • 更偏传统 Node 服务交付:Railway 会比较顺
  • 团队本来就在 Vercel:也完全可以先落在 Vercel

Start 的好处在于,它不想天然把你导向某一个平台。

6. 常见组合

组合一:营销页 + 应用页混合站点

  • 公开页面走 SSR / Streaming / 预渲染
  • 登录后后台页按需要 selective SSR 或 SPA 化
  • 表单和写操作用 Server Functions
  • 对外 Webhook 和 API 用 Server Routes

组合二:内部系统

  • 主体直接走 SPA Mode
  • 保留 Server Functions 处理业务动作
  • 保留少量 Server Routes 做导出、Webhook、集成

组合三:内容驱动站点

  • 预渲染主路由
  • 用缓存头和 CDN 策略做 ISR
  • 对某些慢数据区块做 Streaming

7. 什么时候它更顺手

  • 你更想显式控制缓存和响应头
  • 你不想把“框架能力”和“平台能力”绑太死
  • 你非常在意一套统一的路由、类型和服务端调用模型

8. 什么时候 Next 更省心

  • 你已经把团队心智完全迁到 RSC-first
  • 你很依赖 Next 现成的社区模板、脚手架和平台闭环
  • 你希望很多事情“按官方约定写就对了”,不想要太多组合空间

小结

TanStack Start 的真正价值,不是“它也有 SSR”。现在 React 全栈框架谁没有 SSR。

它更有意思的地方在于:

  • 路由层足够强
  • Server Functions 很能打
  • SSR / SPA / Selective SSR 可以混着用
  • 缓存和部署思路更开放

如果你对这些点有感觉,它就值得认真试;如果你更想要一条更强约定、更平台一体化的主线,那 Next 依然会更省心。