我最近将一个中型 SaaS 应用从 Next.js App Router 迁移到了 TanStack Start。本文记录了我在这个过程中的收获——包括显著的优势、遇到的坑,以及诚实的取舍。
太长不看(TL;DR):TanStack Start 在类型安全、开发者体验和打包体积上胜出。Next.js 则拥有更庞大的生态系统和更多的主机托管选项。对于采用 TypeScript 优先原则构建复杂单页应用(SPA)的团队来说,TanStack Start 是更好的选择。
应用背景
我所迁移的应用情况如下:
- 47 个路由(包含动态路由与嵌套路由)
- 采用基于 Session 的身份验证
- 约 15 个 API 端点(服务器函数)
- 重度依赖 React Query 进行数据请求
- 部署在 Cloudflare Workers 上
这绝不是一个玩具项目,而是有着付费客户的真正 SaaS 产品。
心智模型的差异
Next.js:文件 = 路由 + Server Component + 数据
Next.js 将路由、渲染和数据请求混合在文件之中。默认情况下 page.tsx 便是 React Server Component,你可以通过顶部的 'use client' 选择切入客户端渲染。数据通常直接在组件内部进行请求。
TanStack Start:路由 = 配置 + Loader + 组件
TanStack Start 更明确地分离了关注点。路由通过 createFileRoute() 进行定义,Loader 专门负责数据请求,而组件则永远是客户端组件。服务器端逻辑一般由服务器端函数(createServerFn())来处理。
很难说哪种方案绝对正确——归根结底它们只是适应不同团队偏好的不同心智模型。
获得了哪些改善
端到端类型安全(End-to-End Type Safety)
这是最显著的优势。在 Next.js 里,我经常不得不将 params 从 unknown 类型强转过来,并手动为搜索参数(search parameters)补充类型声明。但在 TanStack Start 中:
// Next.js — params 默认为 unknown,需要强制类型转换
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug) // params.slug 可能是任何类型
}
// TanStack Start — 拥有完整类型支持,兼具运行时验证
export const Route = createFileRoute('/blog/$slug')({
loader: ({ params }) => getPost(params.slug), // 确保 params.slug 类型为 string
component: BlogPost,
})
在全部 47 个路由里,这彻底消除了一整类曾在运行时爆发的代码错误。
React Query 集成
Next.js 和 React Query 的共存显得异常尴尬。Next.js 想要接管数据请求逻辑,而 React Query 同样想占据主导权。你往往不得不用 initialData 注水(hydration)等不够优雅的方法进行绕道折中,这种做法不仅易错而且令人困惑。
而 TanStack Router + TanStack Query 在设计之初就被设定为无缝协同工作:
export const Route = createFileRoute('/users')({
loader: ({ context }) =>
// 在 loader 中预取,React Query 负责处理余下的工作
context.queryClient.ensureQueryData(usersQueryOptions()),
component: UsersPage,
})
function UsersPage() {
// 数据已经通过 loader 存入缓存库 —— 初次渲染时就不会出现加载状态
const { data } = useQuery(usersQueryOptions())
}
打包体积大小(Bundle Size)
完成迁移后,我们的 JavaScript 打包体积下降了 34%。Next.js App Router 始终伴随着一个非常可观的运行时(runtime)一同被分发,而 TanStack Start 相对更为精简。
变得更为困难的部分
图片优化
不得不说,next/image 确实优秀,然而 TanStack Start 还没有原生对等的实现方案。你需要自主解决图片优化的问题——无论是通过 Cloudflare Images、借助 CDN 或者引入 unpic 这样的第三方组件库。
增量静态重新生成(Incremental Static Regeneration)
如果你的应用深度依赖 ISR 来保证页面性能,这种模式在 TanStack Start 中并不存在可以直接套用的等效设计。你需要重新评估缓存策略,转而利用 CDN 的 cache header 配置或是引入独立的外部专属缓存服务层。
生态系统
某些专属于 Next.js 的配套库(如 next-auth、next-intl 等)并不能做到开箱即用。你必须探寻其它替代品或是从零开始自行封装。对我们团队而言,庞大的 TanStack 生态系统已经足以覆盖需求;然而,如果你一直紧密依赖于某些 Next.js 特有插件,请务必在正式动手迁移之前进行查验。
实施迁移的步骤流程
以下是对我们而言行之有效的节奏方案:
第一阶段:剥离业务核心逻辑 将所有的业务逻辑、API 的调用流程以及数据处理工具全都抽离成了与框架解耦(framework-agnostic)的纯粹代码模块。这大概占用了我们一周的时间,并迅速获得了巨大的回报——这直接强迫我们把早期堆积的大量耦合代码做了彻底的清理改造。
第二阶段:重制应用路由 在 TanStack Start 中逐个重新构建页面路由。由最基础的路由(无 Loader,也不携带查询参数)起步,渐渐朝着复杂结构发力。这消耗了大概两周的时间。
第三阶段:服务器函数转换 将老代码里的 Next.js API 路由和 Server Actions 平替迁移到了 TanStack Start 的服务器函数里。这一步基本都是纯体力的机械搬运。
第四阶段:身份认证重构 依托我们选择的全新鉴权库来重新搭建登录认证底座。这是整个搬运中潜藏风险最高的阶段——记得为此提前预留额外的测试排期。
总计迁移周期:2 名开发者累计投入了 5 周的时间。
我还会再做一次迁移吗?
是的——针对此次的具体使用场景来讲确实如此。仅从代码的类型安全提升这一个层面而言,我们就成功阻止了多个原本可能波及生产环境的代码 Bug 隐患。就针对开发团队的工作流而言,开发者体验获得了立竿见影的实质性跃升。
倘若你要构建的是内容极为庞杂且必须依赖 ISR 机制及图片优化的重型门户网站,Next.js 仍然是显然更加妥当的选择。但如果你是要着手构建一个充斥着海量页面路由和繁复数据交互能力的重度 TypeScript 架构型单页应用,TanStack Start 是完全值得这笔可观的框架迁移成本的。