Next.js is the default React framework. That doesn’t make it the best one.

For most of the last few years, “React app” has meant “Next.js app”. It’s the default in tutorials, job ads and AI coding agents. But on 13 October 2026, TanStack Start 1.0 went stable, and I think it’s now the better choice for most React applications.

How we got here

Next.js was released in October 2016 by Zeit, a hosting company that renamed itself Vercel in 2020. It made server-rendered React easy when that was genuinely hard, and it became the default. In 2023 it moved to the App Router, built around React Server Components, where components run on the server unless you say otherwise.

TanStack came from the other direction. Tanner Linsley released React Query in 2019 to solve the problem most React apps actually have: fetching, caching and updating server data. It became one of the most-used libraries in the ecosystem, and was renamed TanStack Query in 2022 as it grew beyond React. Alongside it came TanStack Table, and then TanStack Router, a router built to be fully type-safe from the ground up.

TanStack Start puts those pieces together into a full-stack framework: TanStack Router for routes, server functions for the backend, TanStack Query for data, and Vite underneath. It’s a client-first React app that renders on the server, rather than a server framework that ships some React.

Sage, the practice platform for financial advisers that I’ve been building, runs on it. It has client records, a pipeline board, retirement modelling and advice documents, so it’s a real app with real complexity. Every TanStack Start snippet below comes from its codebase.

Here’s the short version, from an independent benchmark that built the same app in both frameworks:

Same app, same machineTanStack StartNext.js
Product page, requests per second4.6× more
2,341 req/s
512 req/s
JSON API, requests per second8.6× more
18,515 req/s
2,165 req/s
Server memory at idle1.6× less
60 MB
94 MB
Production build3.9× less
1.26 s
4.86 s
Dev server, cold start to first page1.9× less
1.52 s
2.96 s
Deploy output27× less
1.4 MB
38 MB

Source: TanStack Start vs Next.js in 2026: Same App, Real Benchmarks. Next.js 16.4.2 vs TanStack Start 1.169.3, a small store app on Node 24 with two pinned CPU cores. The author's caveat stands: your database, auth and caching will usually dominate.

That’s a small synthetic store. A second study by Loren Stewart built a full kanban app, with drag and drop, comments and optimistic updates, in ten frameworks, and measured what each one ships:

JavaScript shipped by the same kanban board (compressed)
TanStack
118.2 kB
Next.js
176.1 kB

About a third less JavaScript for the same app, on the same React 19.

Source: I Built the Same App 10 Times (Loren Stewart): TanStack Start vs Next.js 16, Lighthouse mobile emulation, median of 10 runs.

Stewart’s study also makes an honest point: React itself sets a floor. Frameworks without React’s runtime shipped less again, with SvelteKit at 54.1 kB for the same board. If you can leave React, that’s worth knowing. If you’re staying, Start is the lighter way to do it.

The speed is nice. But it isn’t why I chose it.

Why TanStack Start

  • Type-safe routes and URLs. Paths, params and search params are typed and validated, so a misspelt link or sort option is a compile error, not a broken page.
  • Server functions with middleware. Call server code like a function, with auth and validation in a reusable chain.
  • Caching you can see. Data is cached and invalidated by key with TanStack Query, and the devtools show exactly what’s stored and when it’s stale.
  • Client-first, server when you want it. Pages render on the server for the first load and run as a fast client app after. Selective SSR lets you turn server rendering off per route.
  • Plain Vite. A fast dev server, flat memory use, and every Vite plugin just works.
  • Deploys anywhere. It builds with Nitro for Cloudflare Workers, Netlify, Vercel, Railway, Render or a plain Node server.

Loading and saving data, end to end

Most of a business app is one job repeated: load a record, show it in a form, save the changes, and keep every screen up to date. Here’s how a client record does that in each framework.

Next.js3 files
// app/clients/[id]/page.tsx — a server component
export default async function ClientPage({
  params,
}: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const client = await getClient(id);
  return <ClientForm client={client} />;
}

// app/clients/[id]/client-form.tsx
// The form needs state, so it moves to its own file.
"use client";

export function ClientForm({
  client,
}: { client: Client }) {
  const [pending, startTransition] = useTransition();
  const save = (formData: FormData) =>
    startTransition(() =>
      updateClient(client.id, formData),
    );
  return <form action={save}>…</form>;
}

// app/clients/actions.ts
"use server";

export async function updateClient(
  id: string,
  formData: FormData,
) {
  await db.update(client).set(parse(formData));
  // Invalidate by URL: name every page
  // that shows this client.
  revalidatePath("/clients");
  revalidatePath(`/clients/${id}`);
}
TanStack Start2 files
// routes/(app)/_layout/clients/$clientId.tsx
export const Route = createFileRoute(
  "/(app)/_layout/clients/$clientId",
)({
  // On the server for the first load,
  // in the browser for every navigation after.
  loader: ({ params, context: { queryClient } }) =>
    queryClient.fetchQuery({
      queryKey: clientKeys.detail(params.clientId),
      queryFn: () =>
        getClient({
          data: { clientId: params.clientId },
        }),
    }),
  component: ClientPage,
});

function ClientPage() {
  const client = Route.useLoaderData();
  const update = useUpdateClient();

  return (
    <ClientForm
      client={client}
      saving={update.isPending}
      onSave={(data) =>
        update.mutate({ clientId: client.id, ...data })
      }
    />
  );
}

// features/clients/hooks.ts
// Invalidate by data: every list and view of this
// client refreshes, wherever it's shown.
export function useUpdateClient() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (data) => updateClient({ data }),
    onSuccess: (_, { clientId }) => {
      queryClient.invalidateQueries({
        queryKey: clientKeys.list(),
      });
      queryClient.invalidateQueries({
        queryKey: clientKeys.detail(clientId),
      });
    },
  });
}
Loading and saving a client. One splits server and client and invalidates URLs; the other invalidates data.

In Next.js, the page is a server component, so the moment the form needs state it has to move into a separate "use client" file. After saving, you invalidate by path, which means remembering every page that might show this client.

In TanStack Start, the route’s loader fills the query cache on the server, and the component reads the same data with no second request and no loading spinner. Saving calls a server function, and useUpdateClient invalidates the client’s query key, so every list, panel and dashboard widget showing that client refreshes, wherever it lives.

Server functions with real middleware

Both frameworks let you call server code from the client without writing an API. The difference is what surrounds that call.

Next.js server actions have no middleware of their own, so each one repeats its own auth check, or you add a library to wrap them. In TanStack Start, createServerFn is a small pipeline: middleware, then validation, then your handler. Sage’s authMiddleware checks the session once and hands a typed context.session to every server function that uses it.

Next.jsapp/clients/actions.ts
// app/clients/actions.ts
"use server";

export async function getClients(input: unknown) {
  // Server actions have no middleware, so every
  // one repeats its own auth check.
  const session = await auth.api.getSession({
    headers: await headers(),
  });
  if (!session?.user) throw new Error("Unauthorized");

  const data = getClientsSchema.parse(input);
  const orgId = session.session.activeOrganizationId!;

  return db.query.client.findMany({
    where: eq(client.organizationId, orgId),
    orderBy: [asc(client.lastName)],
  });
}
TanStack Startserver/functions/clients.ts
// Written once, reused by every server function
export const authMiddleware = createMiddleware({
  type: "function",
}).server(async ({ next }) => {
  const session = await auth.api.getSession({
    headers: getRequest().headers,
  });
  if (!session?.user) throw new Error("Unauthorized");
  return next({ context: { session } });
});

export const getClients = createServerFn({
  method: "GET",
})
  .middleware([authMiddleware])
  .validator(getClientsSchema)
  .handler(async ({ context, data }) => {
    // context.session is typed; data is validated
    const orgId =
      context.session.session.activeOrganizationId!;
    return db.query.client.findMany({
      where: eq(client.organizationId, orgId),
      orderBy: [asc(client.lastName)],
    });
  });
Auth, validation and org scoping. In Sage, every server function starts with the same two lines.

Every piece of data in Sage belongs to a practice, so every query has to be scoped to the user’s organisation. With middleware, that rule lives in one place instead of being copied into dozens of functions and hoping none get missed.

It’s just Vite

TanStack Start runs on Vite. That sounds like a detail, but it changes the day-to-day experience:

  • The dev server stays fast. In the benchmark, the first page loaded in 1.5 seconds against Next.js’s 3.0 seconds, and the production build took 1.3 seconds against 4.9.
  • Memory stays flat. The Start server idled at 60 MB against 94 MB for Next.js, and under load it peaked at about 360 MB against 500 MB.
  • The ecosystem just works. Vitest, Tailwind’s Vite plugin and any Vite plugin you already use drop straight in.
  • The output is small. The benchmark’s deploy output was 1.4 MB, against 38 MB for Next.js.

What about Server Components?

React Server Components are Next.js’s big idea, and the App Router is built around them. Every component runs on the server unless you mark its file "use client". For a content-heavy site, that can mean less JavaScript in the browser. For the kind of app most of us build, I think they get in the way.

  • Server by default is the wrong default for apps. Most React apps sit behind a login: dashboards, CRMs, internal tools. The data is per user, search engines never see it, and nearly every screen is interactive. Those apps gain little from rendering on the server, and pay for it in complexity.
  • They split React in two. A server component can’t use state, effects or context, and a client component can’t import a server component. Components stop composing freely: you thread server components through as children, and add another "use client" file each time something needs to be interactive.
  • The boundary is easy to get wrong. Use a hook in the wrong file and the build fails. Pass a function from a server component to a client one and it breaks at render time.

TanStack Start takes the opposite approach. Components are ordinary React components that render on the server for the first load and in the browser after that. Server Components are available when they genuinely help, but you opt in rather than opt out.

Where Next.js still wins

One place: Vercel. If you’re all in on Vercel’s platform, Next.js is the most integrated way to use it, with features like image optimisation and incremental static regeneration working there with no setup.

That tight coupling is also its biggest criticism. Next.js is often accused of being a funnel for selling Vercel compute. It does run elsewhere, but projects like OpenNext exist largely to make it run well off Vercel. If you’d rather pick your host on its merits, that’s a point for Start.

Why it’s the future

What makes TanStack Start feel like the future isn’t one feature. It’s that nothing about it is magic. Routes are typed objects, server functions are plain functions with a middleware chain, the cache is a key you can inspect, and the build tool is the same Vite the rest of the ecosystem uses. When something goes wrong, you can see why.

Now that 1.0 has committed to its APIs, the main reason to wait is gone. If you’re starting a React application this year, especially one with logins, forms and a lot of data, start with TanStack Start.