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 = RC (1.x, pre-GA). The official TanStack site labels it "RC / preparing for 1.0". 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βœ…βœ…βœ…
RSCβœ…βš οΈ Experimental❌
PPR⚠️ Experimental❌❌
Binary compileβŒβŒβœ…
ISR in dev❌❌❌

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 exportβœ… output: "export"βœ…βœ…
Multi-target builds❌ (Vercel-centric)⚠️ Adapter-basedβœ… (bun, static, planned: vercel, cloudflare)

πŸ’‘ Furin advantage β€” compile: "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.


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)❌❌ (planned)
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)❌❌ (planned)
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)βœ… (Await + defer)βœ… (renderToReadableStream + 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 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 advantage β€” createRoute() 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: April 23, 2026. Sources: Next.js 15 docs, TanStack Start RC docs (v1.x, pre-GA), Furin source code, @tanstack/react-start GitHub (packages/react-start, start-server-core, start-plugin-core).

Comments