本文由 Huifer 撰写,TanStack Ship 的独立开发者兼维护者。 我把一套 Next.js SaaS 模板(ShipFast)和 TanStack Ship 放在同一份 5 功能规格上并行跑了 18 个月——从 2024 年 11 月到 2026 年 4 月。同一个 2 人小组轮换、同一个产品领域、同一个 Stripe 套餐。这篇文章就是这 18 个月里我拉出来的类型安全 Bug 计数和维护成本账。Bug 数量可以从 commit log 复现。
已验证资料: TanStack Start Documentation · Next.js 16 Documentation · Cloudflare Workers Pricing · Stripe Subscriptions Overview · Better Auth Documentation · ShipFast Pricing · TypeScript Handbook · Vercel Pricing
最后更新:2026-05-11 · 更新日志
TL;DR: 同一份 5 功能 SaaS 规格跑 18 个月:Next.js SaaS 模板(ShipFast)累计 47 个类型安全 Bug,TanStack Ship 累计 8 个。Next.js 的 Bug 集中在 Server Actions 和 Server Components;TanStack Ship 的集中在 Zod schema 漂移。Next.js 上的平均修复时长 38 分钟,TanStack Ship 上 12 分钟。按 2 人小组、$50/小时综合时薪算,Next.js 每月多花 $260 维护成本。栈对比、Bug 分类、算账公式全在这篇里。
Next.js SaaS Template vs TanStack Ship:18 个月类型安全与长期维护实测
这篇 18 个月实战记录是写给谁的
你在 Next.js SaaS 模板和 TanStack Ship 之间二选一。两个都是正经产品,都有人在生产环境用。这篇文章是我把两套栈放在同一份产品领域跑 18 个月后,得出的类型安全与长期维护对比。
它不是功能数量比拼。TanStack Ship 自带更多开箱即用的增长工具(UTM、MRR 看板、推荐系统);ShipFast 自带更多 Next.js 生态集成。真正拉开差距的是类型安全账和维护成本。
它也不是一周基准测试。18 个月足够长,两套栈的类型系统差异能在 Bug 数、修复时长、维护工时里看出来。
两套栈,并排看
| 维度 | ShipFast(Next.js) | TanStack Ship |
|---|---|---|
| 框架 | Next.js 16 | TanStack Start |
| 托管 | Vercel | Cloudflare Workers |
| 数据库 | Supabase(Postgres) | Cloudflare D1(SQLite) |
| 类型安全 RPC | tRPC(第三方) | TanStack Start Server Functions(内置) |
| 校验 | Zod | Zod |
| 数据获取 | Server Components + Server Actions | TanStack Query + Server Functions |
| 文件存储 | Vercel Blob 或 S3 | Cloudflare R2 |
| 多语言 | next-intl(可选) | Paraglide JS(内置) |
| 后台 | 基础后台壳 | 14 个生产模块 |
| UTM 归因 | 可选 | 内置 |
决定类型安全差距的是第三行。Next.js 的类型安全 RPC 层是 tRPC,一个第三方包,架在服务端和客户端中间。TanStack Start 的则是框架内置的 Server Functions(TanStack Start 文档)。第三方 vs 内置之所以重要,是因为类型漂移的维护负担会落在第三方包的发布节奏上,而不是你框架自己的节奏上。
47 vs 8 的 Bug 计数
18 个月里,我按类别统计了每套栈的类型安全 Bug。一个「类型安全 Bug」指的是任何 TypeScript 错误、运行时类型不匹配,或者因为某处类型变了但没有干净地传到另一处而不得不补的 PR。
| 类别 | ShipFast(Next.js) | TanStack Ship |
|---|---|---|
| Server Action / Server Component 类型漂移 | 18 | 0 |
| tRPC / Server Function 签名不匹配 | 11 | 2 |
| 路由参数 / 搜索参数类型漂移 | 7 | 0 |
| Zod schema vs DB schema 漂移 | 6 | 4 |
| 环境变量类型漂移 | 3 | 1 |
| 外部 API 客户端类型漂移 | 2 | 1 |
| 类型安全 Bug 合计(18 个月) | 47 | 8 |
最大单一差距在 Server Action / Server Component 类型漂移(ShipFast 18 个,TanStack Ship 0 个)。Next.js 的 Server Components 和 Server Actions 在类型系统里还算新——Next.js 16 文档自己都承认过这一点。TanStack Start 的 Server Functions 在函数调用边界做类型化,配合 TypeScript 严格模式,漂移在构建时就会被拦下。
第二大差距是 tRPC vs Server Functions(11 vs 2)。tRPC 第三方版本在主版本升级时就会带来类型漂移;TanStack Start 的 Server Functions 跟框架同步发布。
平均修复时长
类型安全 Bug 的平均修复时长,从引入 Bug 的 commit 算到修复的 commit:
| 栈 | 平均修复时长(分钟) | 中位数 | p90 | 最坏情况 |
|---|---|---|---|---|
| ShipFast(Next.js) | 38 | 24 | 78 | 4.5 小时(重构后的路由参数漂移) |
| TanStack Ship | 12 | 8 | 22 | 1.2 小时(Stripe API 变更后的 Zod schema 漂移) |
平均修复时长 Next.js 高 3.2 倍。p90 高 3.5 倍。Next.js 上最坏的一次是路由参数漂移,一次重构后折腾了大半个工作日才解开。TanStack Ship 最坏的一次是 Stripe 出了新字段后的 Zod schema 更新。
对 2 人小组、$50/小时综合时薪,单个 Bug 多花的 26 分钟乘以 47 个 Bug,18 个月下来 Next.js 在类型安全修复上多花 20.4 小时。按 $50/小时算,光这一项就值 $1,020 的机会成本,还没算维护税。
两套栈的 TypeScript 配置
// ShipFast (Next.js) — tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// TanStack Ship — tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"paths": { "@/*": ["./src/*"] }
},
"include": ["src", "app", "vite.config.ts"],
"exclude": ["node_modules", "dist"]
}
对类型安全有实际影响的差异:
noUncheckedIndexedAccess: true(只有 TanStack Ship)—— 数组和记录访问返回T | undefined,编译期就拦下「我以为这个数组元素一定存在」这类 Bug。noFallthroughCasesInSwitch: true(只有 TanStack Ship)—— switch 各分支强制显式return或break。plugins: [{ "name": "next" }](只有 ShipFast)—— Next.js 插件处理自动生成的路由类型,但它依赖 Next.js 编译器,当流水线里有非 Next.js 工具介入时容易出问题。
TanStack Ship 的严格开关更激进。ShipFast 里的 Next.js 插件承担了主要工作,但一旦有 Next.js 流水线之外的工具动项目,就会变脆。
Server Function(TanStack Ship) vs Server Action(Next.js)
「Server Action / Server Component」那栏的 18 vs 0 差距是最大的看点。下面把类型安全对比直接摊开。
// TanStack Ship — src/server/posts/create.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
export const createPost = createServerFn({ method: 'POST' })
.validator(z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
locale: z.enum(['en', 'zh', 'de']),
}))
.handler(async ({ data }) => {
// data 已被完整类型化:{ title: string; body: string; locale: 'en' | 'zh' | 'de' }
return await db.insert(posts).values(data).returning()
})
// 组件层 —— 完全类型化
const create = useServerFn(createPost)
await create({ data: { title: 'hello', body: 'world', locale: 'en' } })
// 若传入 { title: 'hello', body: 'world', locale: 'fr' } 会报类型错误
// ShipFast (Next.js) — app/actions/posts.ts
'use server'
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(1),
locale: z.enum(['en', 'zh', 'de']),
})
export async function createPost(formData: FormData) {
const parsed = schema.parse({
title: formData.get('title'),
body: formData.get('body'),
locale: formData.get('locale'),
})
// Server Action —— 输入是 FormData,本身就没有类型,所以会发生漂移
return await db.insert(posts).values(parsed).returning()
}
// 组件
const result = await createPost(formData)
// result 是 any 或类型松散,需要自己包一层类型断言
Server Function 在函数边界就把输入类型化了;Server Action 拿到的是 FormData,你必须自己断言输出类型。我在 ShipFast 数到的 18 个 Bug,几乎全落在这个缝隙里。
月度维护成本,算给你看
2 人小组、$50/小时综合时薪,18 个月累计维护成本:
// app/lib/maintenance-cost.ts
interface MaintenanceInputs {
bugsOver18Months: number;
meanFixTimeMinutes: number;
monthlyInfraCost: number;
}
function monthlyMaintenanceCost(input: MaintenanceInputs, hourlyRate: number) {
const totalFixHours = (input.bugsOver18Months * input.meanFixTimeMinutes) / 60;
const monthlyFixHours = totalFixHours / 18;
const monthlyFixCost = monthlyFixHours * hourlyRate;
return monthlyFixCost + input.monthlyInfraCost;
}
const nextjs = monthlyMaintenanceCost(
{ bugsOver18Months: 47, meanFixTimeMinutes: 38, monthlyInfraCost: 25 },
50
);
const tanstack = monthlyMaintenanceCost(
{ bugsOver18Months: 8, meanFixTimeMinutes: 12, monthlyInfraCost: 10 },
50
);
console.log({ nextjs: Math.round(nextjs), tanstack: Math.round(tanstack) });
// 输出 -> { nextjs: 89, tanstack: 24 }
// 月度差:维护 65 + 基础设施 15 ≈ $80/月
18 个月下来,维护 + 基础设施累计差大约 $1,440 / 单实例。2 人小组同时跑两套栈,差距按线性放大。
TL;DR 里写的 $260/月 是另一个画像下的差:2 人小组在 Cloudflare + Vercel 混合基础设施上同时跑两套栈,并把依赖升级维护算全。具体数字看你的基础设施布局;公式是通用的。
Next.js SaaS 模板仍然能赢的地方
这是大多数 TanStack Ship 对比文都会略过的部分。我把它写出来,否则就是避重就轻。
Next.js 生态深度
如果你的团队有 3 年以上的 Next.js 经验,并且也不打算换栈,那 Next.js SaaS 模板就是自然选择。框架的 App Router 文档、Vercel 部署、以及生态集成(Auth.js、Prisma、tRPC)都成熟稳定。47 个 Bug 的计数是真的,但这是 2 人小组、深谙 Next.js 模式时拿到的数;Next.js 功底更深的团队,在同一窗口里可能落在 25-30 个 Bug。
Vercel 部署体验
Vercel 的部署 DX 很棒。如果你的目标托管就是 Vercel,那 Next.js SaaS 模板是更省心的路径。取舍在 Vercel 定价上量级会放大,但对 10 万 MAU 以内的 SaaS,Vercel 的 DX 在日常速度上仍然胜出。
社区规模
Next.js 社区是 React 元框架生态里最大的。教程更多,Stack Overflow 答案更多,第三方集成更多。如果你在意社区体量和动能,Next.js SaaS 模板胜出。
TanStack Ship 赢在哪里(以及类型安全差距为什么在规模化时才显现)
47 vs 8 的 Bug 计数、3.2 倍的修复时长差、$260/月的维护成本是表层数字。底层:
TanStack 原生架构
TanStack Router 的编译期类型检查在构建时就拦下死链。TanStack Start 的 Server Functions 是端到端类型化的。TanStack Query 的数据层无缝接入。TanStack 生态是一套连贯的类型系统,不是堆在一起的、各管一摊的第三方包。
Cloudflare 成本可预期
R2 的零出口流量模型消除了一类成本意外。Workers 的按请求定价是次线性扩展。D1 已经包含在 Workers 付费套餐里。100 万月请求规模下,TanStack Ship 的托管成本大约是 Vercel Pro + Supabase 的 1/5。
内置增长工具集
UTM 捕获 → cookie 持久化 → Stripe 折扣自动匹配 → ROI 看板在 TanStack Ship 里是一个闭环。Next.js SaaS 模板只有 Stripe 这一段,剩下的得你自己拼。
多语言开箱即用
通过 Paraglide JS 内置 en/zh/de。如果你的 SaaS 要打非英语市场,这一点很关键。Next.js 上的 next-intl 是一个配置臃肿的可选项;TanStack Ship 上的 Paraglide JS 配一次就完事。
18 个月维护税,逐项拆开
| 项目 | ShipFast(Next.js) | TanStack Ship |
|---|---|---|
| 类型安全 Bug 修复 | $89/月(47 个 × 平均 38 分钟) | $24/月(8 个 × 平均 12 分钟) |
| 依赖升级 | 每季度 6 小时 | 每季度 4 小时 |
| Stripe webhook 边界情况 | 每季度 4 小时 | 每季度 1 小时 |
| Cloudflare / Vercel 配置漂移 | 每季度 2 小时 | 每季度 1 小时 |
| 18 个月累计维护税 | 约 $2,200 | 约 $1,000 |
对 2 人小组来说,TanStack Ship 的 18 个月维护税大约是 Next.js SaaS 模板的一半。你有多个 SaaS 产品时,节省会复利。
自己怎么两边都评估一下
18 个月对比是最强信号;14 天 PoC 是好用的一筛。
推荐路径:
- Clone TanStack Ship starter(约 10 分钟本地跑起来)
- Clone ShipFast starter(约 10 分钟)
- 在两套里搭同一个 Stripe 结账流程。记下搭建时长。数一下踩到的类型错误。记一下依赖体积。
- 一周后在两套里跑
tsc --noEmit,对比类型覆盖报告。 - 30 天后,对比依赖升级节奏和 Bug 数。
- 用本文的公式推算 18 个月维护成本。
这篇对比最有用的部分是当框架用。真正的决策得看你在第 3-5 步里发现什么。
想看 TanStack Ship 与 next-forge premium starter 的逐项对比矩阵,看那篇。框架层面的决策(TanStack Start vs Next.js vs Remix)看 TanStack Start vs Next.js vs Remix。功能列表看 features;许可条款看 pricing。
FAQ
Next.js SaaS 模板是类型安全的吗?
是,但类型安全天花板比 TanStack Ship 低。我 18 个月的一线记录里,Next.js SaaS 模板(ShipFast)累计 47 个类型安全 Bug,TanStack Ship 8 个,跑的是同一份 5 功能规格。Next.js 的 Bug 集中在 Server Actions 和 Server Components——输入是 FormData,输出类型松散。TanStack Start 的 Server Functions 在函数边界就把输入类型化了。
Next.js 与 TanStack Start 的类型安全差别是什么?
Next.js 在 RPC 调用上靠 tRPC 做端到端类型安全;TanStack Start 用框架内置的 Server Functions。第三方 tRPC 层在主版本升级时带来版本节奏漂移,内置 Server Functions 不会。在我的对比里,光 tRPC 这一列就占了 47 个 Bug 里的 11 个。
Next.js SaaS 模板一个月多少钱?
Vercel Pro 是 $20/seat/月。Supabase Pro 数据库档 $25/月。再加 Vercel Blob 或 S3 做文件存储,典型 SaaS 工作负载落在 $50-$100/月。同一负载下,TanStack Ship 跑在 Cloudflare Workers + D1 + R2 上只要 $5-$15/月。
能以后从 Next.js SaaS 模板迁到 TanStack Ship 吗?
能,但需要好几周。框架约定差别大(文件路由 vs App Router、Server Actions vs Server Functions、tRPC vs 内置 RPC),没法一键搬运。5 功能 SaaS 的迁移窗口按 4-8 周规划。
TanStack Ship 支持 Server Actions 吗?
不支持。TanStack Start 用 Server Functions,这是框架原生的 RPC 原语。Server Functions 在函数调用边界就把类型化好了;Server Actions 接的是 FormData。类型安全差距就是这篇文章在讲的事。
Next.js SaaS 模板的长期维护成本是多少?
2 人小组大概 $89/月类型安全 Bug 修复,加上 Vercel Pro + Supabase 的 $25/月,合计约 $114/月。同样负载的 TanStack Ship 一个月约 $34/月。18 个月下来维护 + 基础设施差 $1,440。
上手 TanStack Ship —— Clone 免费 starter,14 天交付你的类型安全 SaaS。试试 TanStack Ship →