Next.js vs TanStack Start vs Furin

Choosing a React meta-framework is not about picking the "best" one — it is about picking the one whose trade-offs match your constraints. This page compares Next.js (App Router), TanStack Start, and Furin across every dimension that matters for production decisions.

Disclaimers

  • Next.js = App Router (stable), not Pages Router.
  • TanStack Start = 1.x stable (released May 2026). The official TanStack site still labels it "RC" in some nav elements, but the npm packages and GitHub releases are on stable 1.x semver. Server functions are stable; RSC is experimental and excluded.
  • Furin = latest stable. Where a feature is planned but not shipped, we say so explicitly.
  • This comparison is maintained by the Furin team. We flag our own limitations with ⚠️ and our advantages with 💡 so you can verify independently.

At a glance

Next.jsTanStack StartFurin
RuntimeNode.jsNode.jsBun
HTTP serverCustom (Node)Vite (dev) / H3 + srvx (prod)Elysia
BundlerTurbopack / webpackViteBun.build
Process modelSeparate dev server + client bundlerVite (dev) / H3 via srvx (prod)Single process
SSG
ISR
SSR
Streaming
Deferred data streaming✅ (defer + <Await>)✅ (defer() + <Await>)
RSC⚠️ Experimental
PPR⚠️ Experimental
Binary compile
ISR in dev⚠️ Partial (loader cache, no background revalidation, deferred Promises not cached)

Architecture & Runtime

DimensionNext.jsTanStack StartFurin
Runtime targetNode.js 18+Node.js 18+Bun only
HTTP frameworkCustom server (minimal abstraction over Node http)Vite (dev) / H3 (prod, via UnJS)Elysia (typed, chainable, WinterCG)
Process model (dev)2 processes: Next.js dev server + Turbopack client bundler1 process: Vite dev server with SSR middleware1 process: Bun serves HTTP + bundles client via Bun.build + plugin pipeline
Process model (prod)1 process (custom server) or serverless1 process (H3/srvx) or serverless1 process (Elysia plugin) or compiled binary
WinterCG compliancePartial (uses Node APIs)Partial (H3 adapts via srvx)Native (Elysia is WinterCG-native)
API routes colocation✅ Route handlers in app/api✅ File-based API routes (H3 server functions)✅ Elysia plugins colocated with pages
WebSocket / SSECustom implementationVia H3 event handlers (crossws)Native Elysia (.ws(), SSE via ReadableStream)

💡 Furin advantage — Single-process dev eliminates the "two servers out of sync" class of bugs. HMR, SSR, and API routes share the same Bun runtime and module graph.

⚠️ Furin limitation — Bun-only means you cannot deploy to standard Node.js containers without switching runtimes. Docker with oven/bun is the recommended path.


Build System

DimensionNext.jsTanStack StartFurin
Build toolTurbopack (dev) / webpack (prod)ViteBun.build
Compilation to binary✅ (compile: "embed")
Output directory.next/.output/ or .tanstack-start/dist/bun/
Client assetsHashed chunks in .next/staticHashed chunks in dist/clientHashed chunks in dist/bun/client
Plugin APITurbopack plugins (unstable) / webpackVite plugins (Rollup-compatible)Bun plugins (native + JS API)
Tree-shaking✅ (webpack) / ⚠️ Turbopack WIP✅ (Rollup)✅ (Bun bundler)
MinificationSWCEsbuild / Rollup terserBun built-in
Source maps✅ (linked)
CSS handlingPostCSS / Tailwind via postcss.config.jsPostCSS / Tailwind via ViteTailwind v4 via bun-plugin-tailwind
Build time (cold)MediumFastFast
Static exportoutput: "export"
Multi-target builds❌ (Vercel-centric)⚠️ Adapter-based✅ (bun, static, planned: vercel, cloudflare)

💡 Furin advantagecompile: "embed" produces a single self-contained binary with all assets baked in. This is unique among React meta-frameworks and ideal for CLI tools, desktop apps, or air-gapped deployments.

⚠️ Furin limitation — The Bun bundler ecosystem is younger than Vite/Rollup. Some advanced transforms may require writing a custom BunPlugin.


Render Modes

DimensionNext.jsTanStack StartFurin
SSR
SSG
ISR✅ (revalidate, revalidatePath)❌ (caching via staleTime + TanStack Query only)✅ (revalidate on route, revalidatePath())
Streaming SSR✅ (React 18 renderToReadableStream)✅ (React 18 renderToReadableStream)
RSC (React Server Components)⚠️ Experimental
PPR (Partial Prerendering)⚠️ Experimental
Server Actions✅ (createServerFn())
Suspense boundaries
Error boundaries✅ (error.js)✅ (React ErrorBoundary)✅ (error.tsx per segment)
Not-found handling✅ (not-found.js)✅ (route fallback)✅ (not-found.tsx per segment)

💡 Furin advantage — ISR is implemented with stale-while-revalidate semantics, ETags, and Cache-Tag headers out of the box. No platform-specific adapter required.

⚠️ Furin limitation — No RSC means every component in the tree is shipped to the client. For apps with heavy server-only logic (data transforms, heavy libraries), this increases bundle size compared to Next.js App Router.

💡 Furin notedefer() is optimized for SSR. SSG and ISR routes support it, but deferred Promises resolve synchronously during the render pass (no streaming benefit).


Dev DX (Developer Experience)

DimensionNext.jsTanStack StartFurin
HMR speedFast (Turbopack)Fast (Vite)Fast (Bun HMR + plugin pipeline)
SSR during HMR
ISR in dev❌ (dev = SSR always)⚠️ Partial (loader data is cached and skipped within the revalidate window; no background revalidation; deferred Promises are not preserved on cache hit)
Cache hit/miss visibilityNEXT_PRIVATE_DEBUG_CACHE=1Console logsBuilt-in logs ([furin] ISR miss / hit / stale)
Startup time (bun dev / next dev)MediumFastFast (single process, no Vite server to boot)
Type-safe links❌ (string-based href)✅ (<Link to="/about">)✅ (<Link to="/about"> with autocomplete)
Typed params / query⚠️ Zod manual✅ (validateSearch, params)✅ (createRoute({ params: t.Object(...) }))
Error overlay✅ (Turbopack)✅ (Vite)✅ (Bun runtime errors)
Source maps in dev
Log streamingnext-log or customCustomevlog structured logging (built-in)

💡 Furin advantage — Built-in structured logs for every cache hit/miss/stale make debugging caching behavior straightforward in both dev and production.

⚠️ Furin limitation — The dev error overlay is less polished than Turbopack or Vite. Stack traces are accurate but formatting is basic.


Caching

DimensionNext.jsTanStack StartFurin
Page cache (SSG)Filesystem (.next/cache)In-memory or customIn-memory LRU (ssgCache)
Page cache (ISR)Filesystem (.next/cache)In-memory or customIn-memory LRU (isrCache)
Data cache (loaders)✅ (fetch cache, unstable_cache)⚠️ Manual (TanStack Query)❌ (loaders re-run on every background revalidation)
Cache persistence✅ Filesystem❌ (in-memory only)❌ (in-memory only)
Cache invalidation by path✅ (revalidatePath)Manual✅ (revalidatePath(path, "page"))
Cache invalidation by layout✅ (revalidatePath(path, "layout"))Manual✅ (revalidatePath(path, "layout"))
Cache invalidation by tag✅ (revalidateTag)✅ (revalidateTag, furinSync, furinInvalidate)
ETags✅ ("buildId:timestamp")
Conditional requests (304)
CDN headerss-maxage, stale-while-revalidateManualCache-Tag, s-maxage, stale-while-revalidate
CDN purger hookVercel-only✅ (setCachePurger())
Client prefetch cacheNext.js router cacheTanStack Router cacheRouterProvider prefetch cache with auto-invalidation
Stale-deploy detection✅ (X-Furin-Build-ID header)

💡 Furin advantage — Built-in CDN semantics (Cache-Tag, s-maxage, stale-while-revalidate, must-revalidate) without platform lock-in. The setCachePurger() hook works with any CDN (Cloudflare, Fastly, custom).

⚠️ Furin limitation — No data cache means ISR background revalidations re-run all loaders from scratch, even if the underlying data has not changed. Next.js unstable_cache can avoid this. A data cache layer is planned.

⚠️ Furin limitation — In-memory cache is lost on process restart. For Docker deployments, this means every deploy or crash wipes the ISR cache. A filesystem-backed cache adapter is planned.


Routing & Layouts

DimensionNext.jsTanStack StartFurin
File-based routing✅ (app/)✅ (src/routes/)✅ (src/pages/)
Nested layouts✅ (layout.js)✅ (__layout.tsx)✅ (_route.tsx)
Dynamic segments✅ ([id], [...slug])✅ ($id, $slug?)✅ ([id], [...slug])
Catch-all routes✅ ([...slug])✅ ($.tsx)✅ ([...slug])
Route groups✅ ((group))
Parallel routes✅ (@team)
Intercepting routes✅ ((.), (..))
API routes colocation✅ (route.js)✅ (api/)✅ (Elysia plugins)
Loading UI✅ (loading.js)❌ (manual Suspense)❌ (manual Suspense)

💡 Furin advantage — Layouts are Elysia routes with loaders, so you get typed params/query and middleware at every nesting level. The _route.tsx file is both a layout and a route boundary (error + not-found).

⚠️ Furin limitation — No route groups, parallel routes, or intercepting routes. These are App Router-specific advanced patterns. If you need them, Next.js is the better choice.


Data Loading

DimensionNext.jsTanStack StartFurin
Route loaders✅ (async function Page())✅ (Route.loader)✅ (createRoute().loader())
Layout loaders✅ (async function Layout())✅ (Route.loader)✅ (_route.tsx loader)
Typed loader output⚠️ (TypeScript inference, no runtime validation)✅ (Zod/Valibot via validateSearch)✅ (TypeBox via params/query on createRoute())
Data cache intermédiaire✅ (fetch cache, unstable_cache)⚠️ (TanStack Query)❌ (planned)
Mutations (server actions)✅ ("use server")✅ (createServerFn())❌ (use API routes)
Redirect from loader✅ (redirect())✅ (throw redirect())✅ (throw redirect() or ctx.redirect())
Not-found from loader✅ (notFound())✅ (throw notFound())✅ (notFound())
Error handlingerror.js boundariesErrorBoundaryerror.tsx per segment
Streaming loader data✅ (RSC streaming)✅ (defer + <Await>)✅ (defer() + <Await> + <Suspense>)

💡 Furin advantage — Loaders are standard async functions with TypeBox schemas for params/query. No magic conventions: what you see in createRoute() is what gets validated at runtime.

⚠️ Furin limitation — No server actions. Mutations must go through Elysia API routes (/api/*) or external endpoints. This is explicit but more verbose than Next.js "use server".


Type Safety

DimensionNext.jsTanStack StartFurin
Typed links (href)❌ (strings)✅ (to={"/about"})✅ (to={"/about"} with autocomplete)
Typed search params❌ (manual)✅ (validateSearch)✅ (query: t.Object(...))
Typed search read / update❌ (manual useSearchParams)✅ (useSearch, useSearchParams)✅ (const [search, setSearch] = useSearch("/products"))
Typed route params❌ (manual params: Promise<{ id: string }>)✅ (params)✅ (params: t.Object(...))
Typed API routes❌ (manual)⚠️ (TanStack Router types, no native E2E API inference)✅ (Elysia tRPC-like inference)
End-to-end type inference✅ (TanStack Query + Router)⚠️ (Router + client types, no Query equivalent)
Generated manifest✅ (routeTree.gen.ts)✅ (furin-env.d.ts)

💡 Furin advantagecreateRoute() is the single source of truth for params, query, and loader types. The generated furin-env.d.ts gives autocomplete on <Link> without any runtime overhead.

⚠️ Furin limitation — No equivalent to TanStack Query for client-side data fetching. If you need complex client caching, deduplication, and background refetch, you must bring your own data library.


Deployment

DimensionNext.jsTanStack StartFurin
Self-hosted (Docker)✅ (next start)✅ (srvx/H3 server)✅ (bun run server.ts or compiled binary)
Static export✅ (output: "export")✅ (static target)
Vercel (serverless)✅ (native)✅ (via srvx adapters)❌ (planned)
Cloudflare Workers⚠️ (edge runtime)✅ (official partner, native guide)❌ (planned)
AWS Lambda⚠️ (adapter)✅ (via srvx/H3 adapters)❌ (planned)
Edge runtime✅ (Edge API Routes)⚠️ (H3/srvx, adapter-based)⚠️ (Elysia is WinterCG, but no edge adapter yet)
Embedded binary✅ (compile: "embed")
Single-file deploy✅ (compiled binary ~20-50 MB)

💡 Furin advantage — The compiled binary (compile: "embed") is a unique deployment artifact. One file, no node_modules, no Docker image. Copy it to a server and run it. This is ideal for internal tools, offline environments, or simple VPS setups.

⚠️ Furin limitation — Platform adapters (Vercel, Cloudflare) are not yet implemented. If you need serverless/edge deployment today, Next.js or TanStack Start are better choices.


When to choose which

Choose Furin if...

  • You are already using Bun and want to stay in a single runtime.
  • You want one process for dev and prod (no separate bundler + server).
  • You need an embeddable binary (CLI tools, desktop apps, air-gapped deployments).
  • You prefer Elysia as your HTTP framework (typed, chainable, fast).
  • You want CDN-ready cache headers without platform lock-in.

Choose Next.js if...

  • You deploy to Vercel (native integration, edge functions, analytics).
  • You need React Server Components (zero client JS for server-only UI).
  • You need Server Actions for mutations without API routes.
  • You use advanced routing (parallel routes, intercepting routes, route groups).
  • You need the largest ecosystem (plugins, examples, hiring pool).

Choose TanStack Start if...

  • You want a library-first approach (bring your own UI, styling, state).
  • You prefer Vite and the Rollup ecosystem.
  • You need TanStack Router features (nested routing, search param validation, breadcrumbs).
  • You want TanStack Query integration out of the box.
  • You prefer H3/UnJS for server primitives.

Summary table

You need...Choose
RSC / Server ActionsNext.js
Vercel edge / serverlessNext.js
Embeddable binaryFurin
Bun-native single processFurin
Vite + library-firstTanStack Start
TanStack Query + RouterTanStack Start
Maximum ecosystem maturityNext.js
Maximum transparency / no magicFurin

Last verified: May 9, 2026. Sources: Next.js 15 docs, TanStack Start 1.x stable docs, Furin source code, @tanstack/react-start GitHub (packages/react-start, start-server-core, start-plugin-core).

Comments