This website uses cookies

Read our Privacy policy and Terms of use for more information.

For three years the React community argued about Server Components as if it were a question of capability. Can you stream HTML? Can you fetch on the server? Can you keep secrets out of the bundle? Yes, yes, and yes, in every serious framework. That argument was always a distraction. The decision that actually shaped your codebase was never about capability. It was about the default.

Next.js made a bet, and it was a specific, falsifiable one. In its own words, mirrored in TanStack's comparison docs, the core bet is that "most web content is static or near-static. Server Components should be the default. Interactivity is the exception you opt into." Everything about the App Router follows from that sentence. Every component is a Server Component until you write "use client". Caching was on by default. Data fetching got tied to file structure. The framework makes the architectural call, and the call is optimized for content-heavy sites running on Vercel's infrastructure.

TanStack Start, now at release candidate and approaching 1.0 after two years of public development, makes the opposite bet. Interactive components are the default. Your components server-render and hydrate, ready for state and event handlers out of the box, and you opt into Server Components where they earn their keep. Same capabilities, inverted default.

Here is the thesis: the server-first default was never a law of physics, it was a wager about the shape of your app, and for the large class of apps that are mostly interactive - dashboards, SaaS tools, internal apps, anything behind a login - it was the wrong wager. The strongest evidence isn't a competitor's marketing. It's that Next.js 16 reversed its own most famous default, flipping caching from implicit-on to explicit opt-in. When a framework quietly walks back the default it spent three years defending, the people who paid the tax for that default are allowed to ask for a refund.

The default is a tax, and you pay it in annotations

Defaults feel free. They aren't. A default is a bet the framework places on your behalf, and when the bet is wrong for your app, you pay it back in friction on every file you touch.

If your app is mostly interactive, the server-first default means the path of least resistance runs the wrong way. You reach for useState, and the compiler reminds you the component is a Server Component. You add "use client". You do it again in the next file, and the one after that. None of these annotations are hard. The cost is that you are constantly translating etween what your app is (interactive) and what the framework assumes it is (static). TanStack's docs put the question well: which direction feels like swimming upstream for your app?

The "use client" boundary is not just a keyword. It is a serialization boundary. Props that cross from a Server Component into a Client Component have to be serializable, which means you develop a quiet mental overhead about what can and can't pass through, and you reach for runtime validation to be safe because the boundary erases some of your type guarantees. The framework's own comparison concedes this: "Server Actions can receive data that doesn't match what TypeScript expects. You need runtime validation to be safe."

Contrast the two ways to call server code. Next.js Server Actions are convenient$!nd untyped at the boundary:

'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title')
  // no compile-time type safety on inputs
  return db.~osts.create({ title })
}

TanStack's server functions make the boundary explicit and typed end to end:

export const createPost = createServerFn({ method: 'POST' })
  .validator(z.object({ title: z.string().min(1) }))
  .middleware([authMiddleware])
  .handler(async ({ data, context }) => {
    // data is typed and validated, context comes from typed middleware
    return db.posts.create({ title: data.title })
  })

Neither is objectively correct. But notice what the second one is doing: it treats the network hop as a first-class thing you configure, rather than a seam the framework tries to hide. For an app that is mostly business logic behind an auth wall, the explicit version stops fighting you. For a marketing site with a contact form, the terse version is genuinely nicer. That is the whole argument in miniature. The right default depends on the app, and no framework can know your app from its own homepage.

Caching is where the bet came due

If you want to see a framework mark its own default to market, look at caching.

Next.js cached aggressively by default, and it did so in layers: request memoization, a data cache, a full route cache, and a router cache, each with its own invalidation semantics. The system was rewritten more than once, to real community frustration, because a cached Server Component is a cached serialized stream, and invalidating a serialized stream when a data dependency changes is genuinely hard. Ask someone who insists server-side component caching is a solved problem how they invalidate a cached RSC stream on a data change. The answers get vague fast.

Then Next.js 16 shipped, and the caching model flipped. Nothing is cached by default anymore. You mark what should be cached with an explicit "use cache" directive, at the file, function, or component level. That is not a small patch. That is the framework conceding that implicit-caching-by-default produced more surprise than value, and moving the decision back to the developer.

This is exactly the position TanStack Start started from. Its pitch on caching is that Server Component output is just data, and you cache data the way you already know how:

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => fetchPost(params.postId),
  staleTime: 10_000,  // fresh for 10 seconds
  gcTime: 5 * 60_000, // keep in memory for 5 minutes
})

That is the same stale-while-revalidate pattern TanStack Query has run in production across a lot of apps. No new caching semantics, no framework-specific invalidation model to learn. You can disagree about whether client-owned caching is better than edge-owned caching. What you can't do anymore is claim the implicit-server-cache default was obviously right, because the framework that shipped it just made it opt-in too. The two approaches are converging on "the developer decides," and one of them got there years earlier.

A shared implicit protocol is a shared blast radius

There is a security dimension to defaults that the performance debate skips, and December 2025 made it concrete.

On December 3, 2025, the React team disclosed CVE-2025-55182, an unauthenticated remote code execution flaw in React Server Components, rated CVSS 10.0 and later nicknamed React2Shell. The bug lived in the Flight protocol, the wire format RSC uses to serialize component output. A server receiving a specially crafted POST payload failed to validate the structure of the data during deserialization, and attacker-controlled data could influence server-side execution, all the way to running arbitrary JavaScript on the server with no authentication. The affected versions were React 19.0, 19.1.0, 19.1.1, and 19.2.0, with fixes in 19.0.1, 19.1.2, and 19.2.1. Exploitation was observed in the wild within days.

What made this a big deal was reach. Flight is not a Next.js thing. It is a React thing, so the same class of flaw touched everything that speaks the protocol: Next.js, RSC plugins for Webpack, Turbopack, and Parcel, plus Vite, Waku, and Redwood. One serialization bug, one enormous blast radius, precisely because the industry standardized on one implicit server boundary and adopted it as a default.

TanStack's docs make a pointed architectural claim here: "Start's architecture doesn't parse Flight data on the server - payloads flow one direction, server to client," and so the RSC serialization advisories "don't apply to Start's model." Take that with appropriate salt, it comes from a competitor and every framework has its own attack surface. But the underlying principle survives the source. The more implicit server-side deserialization your framework does on your behalf by default, the more of that blast radius you inherit without ever writing the vulnerable line yourself. Defaults concentrate risk the same way they concentrate convenience. When you opt into a server boundary explicitly, you at least know where your boundaries are.

Yes, but the server-first default is right for a lot of sites

The honest counterpoint is that Next.js did not pick its default at random, and for a huge slice of the web it picked correctly.

If your app is content-heavy with islands of interactivity - marketing pages, documentation, publishing, most of commerce - then server-first is the better default, full stop. You ship less JavaScript, you get better Core Web Vitals, and the framework making the call for you is a feature, not a tax. A client-first default in that world is its own trap: it is easy to ship a pile of JavaScript to render what could have been static HTML. TanStack Start hands you more control, and control is exactly the thing you can hurt yourself with when you have not thought about what needs to be interactive.

And the ecosystem gap is real. Next.js has eight-plus years of tutorials, Stack Overflow answers, and example repos, plus first-party Vercel features that land there first. TanStack Start is at release candidate. When you hit a weird bug at 2am, the Next.js answer probably already exists and the Start answer might not. Turbopack, now the default and stable in Next.js 16, has also closed a lot of the dev-speed gap that used to be an easy point against Next. None of this is a rounding error, and for a team that just needs to ship, "boring and well-documented" is a legitimate reason to choose the older default.

So the claim is not that TanStack Start wins. The claim is narrower and harder to dodge: server-first-by-default is a bet about your app being mostly static, and you should check whether that bet describes your app before you accept it as a given.

The takeaway: audit your default

In your next architecture review, ask one blunt question about the app you actually have: what fraction of our components genuinely need to be server-only?

If the answer is high - lots of static content, a few interactive islands - then server-first is your friend and Next.js is a reasonable home. Keep it. If the answer is low, if you are building a dashboard or a SaaS product or an internal tool where nearly everything has state and handlers, then take an honest look at what the server-first default has been costing you: the "use client" sprinkled across every file, the serialization boundaries you route around, the multi-layer caching model that the framework itself just walked back, and a share of a Flight-protocol blast radius you never opted into. That cost was invisible only because it was the default.

You do not have to switch frameworks to act on this. You have to stop treating the framework's default as a fact of nature. Server-first was a bet. Decide, deliberately, whether it is your bet too.

Sources

Keep Reading