原文:Reducing local dev time by 83%: Why we migrated off Next.js 翻译:TUARAN 欢迎关注 {{前端周刊}},每周更新国外论坛的前端热门文章,紧跟时事,掌握前端技术动态。
Inngest 团队非常重视开发者体验(DX)。但当本地开发时页面首次加载要等 10–12 秒,“把体验做漂亮”就会变成一种消耗。
本文讲述他们为什么、以及如何从 Next.js 迁出,转向 TanStack Start,并在迁移后把本地首次加载时间大幅降到 2–3 秒(作者给出的量化结果是减少 83%)。

作者加入 Inngest 时,团队已经深度使用 Next.js:
当时的承诺很诱人:摆脱 SPA 的空白 loading 与瀑布式请求,获得嵌套布局与 streaming,并把技术栈收敛到一个框架。
但“蜜月期”很快结束。作者认为 Next.js 优化的是一种特定工作流:有专门前端团队、长期深耕框架细节。而对他们这种小团队(多数工程师要多线作战)来说,认知负担会不断累积:
"use client" / "use server" 的边界这些都让非“全职前端”的工程师感觉是在和框架搏斗,而不是在交付功能。
他们先尝试弱化 RSC:尽量只用最少量的 server components,并偏好 client components。短期内 DX 变得可接受了一些。
但随之而来的问题是:变慢——非常慢。
本地开发环境的首次页面加载时间推到至少 10–12 秒。
Slack 里不断出现抱怨:“我讨厌这个。”“前端太慢了。”
最终大家达成一致:我们的开发者体验很糟。
他们尝试升级 Next.js,并使用 Vercel 的 profiling 工具评估效果,但没有改善。
接着试 Turbopack(还试了两次)。对一个规模不小的代码库来说,这不轻松:需要升级依赖与做不少重构。
更麻烦的是:当时 Vercel 生产环境构建仍主要支持 Webpack,导致本地开发与生产构建链路不一致,带来额外问题。
最终 Turbopack 对本地首屏加载的改善有限,平均也就快了几秒。
作者的结论很直白:Turbopack 并没有那么 turbo,是时候看看 Next.js 之外的选择。
他们希望得到:
于是原型验证了三种选择:
作者以前用过 Fresh 与 Remix,都认可它们的成熟度。但:
综合权衡后,他们决定押注 TanStack Start。

他们需要在两种方式中选:
为了估算成本,作者先从 Dev Server(dashboard 路由的一个子集)开始试转。结果转得比预期快,于是直接一路做到底:
迁移过程中,共享组件凡是依赖 Next.js 的地方,就复制一份改成 TanStack 等价实现;遇到 app heads 之间的交叉引用,也用一些临时类型 hack 过渡。
迁移后,他们的本地开发体验显著变好:
作者强调:这与 Next.js 形成鲜明对比——在他们的体验里,Next.js 本地开发时“每个路由的首次加载”都容易很慢。
他们认为核心差异在于:
作者用两个片段对比了 Next.js App Router 与 TanStack Router 的风格差异。
export default async function RootLayout({
params: { environmentSlug },
children,
}: RootLayoutProps) {
const env = await getEnv(environmentSlug);
return (
<>
<Layout activeEnv={env}>
<Env env={env}>
<SharedContextProvider>{children}</SharedContextProvider>
</Env>
</Layout>
</>
);
}布局与服务端数据获取“混在一起”,唯一提示它在服务端:async/await。
export const Route = createFileRoute('/_authed/env/$envSlug')({
component: EnvLayout,
notFoundComponent: NotFound,
loader: async ({ params }) => {
const env = await getEnvironment({
data: { environmentSlug: params.envSlug },
});
if (params.envSlug && !env) {
throw notFound({ data: { error: 'Environment not found' } });
}
return { env };
},
});
function EnvLayout() {
const { env } = Route.useLoaderData();
return (
<>
<EnvironmentProvider env={env}>
<SharedContextProvider>
<Outlet />
</SharedContextProvider>
</EnvironmentProvider>
</>
);
}作者解释:这里的 getEnvironment 是 createServerFn,只会在 server 执行;useLoaderData 则在 client 侧读取路由数据。

他们对 AI 的定位很务实:主要让 AI 做“体力活”,而不是做架构决策。
做法大致是:
此外,AI 也用来辅助处理一些 TypeScript 与边角 bug。
作者把迁移结果开源在 UI monorepo:
github.com/inngest/inn…