Why You Might Still Pick Next.js Over TanStack Start
From a TanStack Start fanboy
TanStack Start is exciting. File-based routing, type-safe search params, and a stack that feels modern and, most importantly, lets you keep your SPA goodies and cool libraries like TanStack Query to maximize UX for your users and DX for yourself. We all know how heavy-handed mutations can be in Next.js, but with TanStack it all feels like a breeze. Lately, with server components quietly encroaching on Next.js territory, and a rising number of job postings asking for TanStack skills, the ecosystem is clearly growing.
That said, Next.js still wins in a few places that matter in production. This isn't a dunk on TanStack. It's a short list of reasons you might still choose Next.js, especially for mostly-static content sites.
1. Static Server State, Prebuild, and ISR
To be clear: this is not about TanStack Query client caching. This is about static server state: how Server Components and page output get stored, served, and regenerated on the server (Next's Full Route Cache / Data Cache style setup). Off Vercel that usually means wiring a KV (or similar) so the framework can actually persist that page-level server state. On Vercel, a lot of that plumbing is already there.
Next.js makes this easy. You declare revalidation once and the framework owns the rest:
// Next.js -- page-level ISR / static server state
export const revalidate = 3600 // regenerate at most once per hour
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <Article post={post} />
}
TanStack Start does have prebuild capabilities, but you still end up manually setting cache headers (or whatever your platform expects) to get proper edge/CDN behavior. Cloudflare made this friendlier with Workers Cache and solid stale-while-revalidate support (so the pieces exist), but Next's declarative revalidate, revalidatePath, and revalidateTag story is cleaner when SEO and freshness matter at scale. Less platform glue, more "set a number and ship."
2. Suspense-Aware Routing (and the Query-Param Flicker)
Next's data fetching and router integrate more cleanly with Suspense. TanStack Router leans on an external store for a lot of its state, and that store doesn't register with Suspense the way you'd hope.
A common pain point: a list driven by search params (filters, page, sort). Change a param, TanStack tracks it, your useSuspenseQuery re-suspends, and the Suspense boundary flickers.
In Next.js you'd wrap the update in useTransition so React keeps showing the old UI until the new data is ready. Same trick on TanStack Router / Start? Mostly a no-op: the navigate still lands, the query still suspends, and you still get the fallback flash.
Workarounds (neither ideal):
- Swap
useSuspenseQueryforuseQuery+keepPreviousDataso you never hit a Suspense boundary on param changes. Old rows stay on screen; dim them withisFetching/isPlaceholderData. - If you're on a fully suspenseful library (e.g. Relay), you're mostly stuck. Compose so controls (search, pagination) live outside the suspending region and only the data pane suspends. Filters stay mounted, but you still get flicker in the list where
startTransitionwould have held previous data.
Same patterns as the demos above, side by side:
// Pain: useSuspenseQuery + startTransition (no-op dampening)
function ProductsPage() {
const { page, q } = Route.useSearch()
const navigate = Route.useNavigate()
const [isPending, startTransition] = useTransition()
// Looks like the Next pattern. Does not hold previous UI here.
// Router search updates aren't coordinated with Suspense the way
// React state + use() are.
function goToPage(nextPage: number) {
startTransition(() => {
navigate({ search: (prev) => ({ ...prev, page: nextPage }) })
})
}
return (
<>
<Filters onPageChange={goToPage} />
<div className={isPending ? "opacity-60" : ""}>
<Suspense fallback={<ListSkeleton />}>
<ProductResults page={page} q={q} />
</Suspense>
</div>
</>
)
}
function ProductResults({ page, q }: { page: number; q: string }) {
// Changing page / q re-suspends → fallback flash
// (even when navigate was wrapped in startTransition)
const { data } = useSuspenseQuery(productsQueryOptions({ page, q }))
return <ProductList products={data} />
}
// Workaround: useQuery + keepPreviousData (no Suspense flicker)
function ProductsPageKeepPrevious() {
const { page, q } = Route.useSearch()
const { data, isFetching, isPlaceholderData } = useQuery({
...productsQueryOptions({ page, q }),
placeholderData: keepPreviousData,
})
return (
<>
<Filters />
{/* dim via isFetching / isPlaceholderData */}
<ProductList products={data} />
</>
)
}
// Next contrast: startTransition *does* hold the previous list
const [isPending, startTransition] = useTransition()
function selectColor(next: string) {
startTransition(() => setColor(next)) // keeps old UI while Results suspends
}
3. Image and Font Optimization
For mostly-static sites chasing Lighthouse / Core Web Vitals, Next's next/image and next/font are still a real advantage. TanStack is working on this space, but today the built-in story isn't as strong. If those scores are a hard requirement, Next is the safer bet.
import Image from 'next/image'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function Hero() {
return (
<div className={inter.className}>
<Image src={hero} alt="" width={1200} height={630} priority sizes="100vw" />
</div>
)
}
next/image handles responsive sizes, lazy loading, and WebP/AVIF conversion out of the box. next/font subsets and self-hosts fonts to reduce CLS. In TanStack Start, you'd reach for Vite plugins like vite-imagetools or manual preload links. Both work, but require more configuration and ongoing maintenance.
4. React Canary Features (View Transitions) Built In
Another Next.js W: App Router ships on React canary, so features like the <ViewTransition> component are available without bolting on a separate experimental React install. Navigations already run inside React's startTransition, so the React-aware View Transition path can participate in the render lifecycle.
TanStack Start does support view transitions, but I've hit flashes of white before the animation starts. Part of that is the transition firing outside React's startTransition / Suspense coordination: blank paint, then the morph. It's also because the current TanStack implementation is the standard web View Transitions API: no React integration. Next gives you React's canary <ViewTransition> component, which actually participates in React's render lifecycle. Same feature name, very different depth of integration, and the React-aware path feels smoother out of the box.
Tanstack version has white flashes that do not appear in the nextjs one with the same style of css
To remedy this I found that addinga display none to the old view helped
Before (flash / underlay linger)
[data-style="wipe"]::view-transition-old(root) {
animation: none; /* old page stays under the wipe */
}
[data-style="wipe"]::view-transition-new(root) {
animation-name: reveal-dark;
}
The incoming page animates, but the outgoing snapshot stays painted underneath — so you see a blank/flash before the wipe finishes.
After (no flash)
[data-style="wipe"]::view-transition-old(root) {
display: none; /* drop old snapshot immediately */
}
[data-style="wipe"]::view-transition-new(root) {
animation-name: reveal-dark;
}
Only the new view participates. Same pattern on Next with React’s class target — swap root for .canvas-vt.
5. Ecosystem Fit
This one's more personal (it might not move the needle for everyone), but it matters a lot if you live in that stack.
Payload CMS is a strong pick when you're already on Next.js. It ties into the App Router cleanly, gives you a highly customizable backend without building half of it yourself, and leans on Server Components to get a lot done. That's a plus TanStack Start simply doesn't have today.
Big caveat: the Payload team has said they're working on TanStack support. After Start gained Server Components, they've been taking it more seriously. They've talked about a TanStack integration landing before the next major Payload release. Until that ships, if you want Payload in your React app today, Next.js is the option.
Closing: When I'd Still Bite the Complexity and Pick TanStack Start
Most of these gaps will shrink as Start matures (including a real Payload integration). For static, crawlable content where ISR, Suspense-friendly nav, image/font SEO, canary React features, or Next-native CMS matter, Next still has the edge.
But I'm a Start fanboy for a reason. Classic fit: a light landing page attached to a heavy, auth-gated dashboard. No bot crawls it. First paint of 80ms vs 800ms barely matters when the user is about to wrestle charts and filters. Paying Next's full server-render + static-server-state tax for that UI is wasted compute and DX.
What you gain instead:
-
Full
useQuerypower: fine-grained fetches, optimistic updates, cache as source of truth, without the App Router's mutation → revalidate → stream-diff loop. -
Typed search params in the router. Share a URL; coworkers land on the same filters. In Next you'd bolt on
nuqs; in TanStack Router it's first-class. - Rendering escape hatches: data-only (hydrate from server-fetched data) or SSR off. Dump heavy charting into the browser instead of keeping server compute warm for dashboards no bot will score.
Doing that shape in Next is hard. Wiring Query into the App Router fights you toward server actions that invalidate and re-render the whole page. For a per-user, constantly moving dashboard, that's a quiet inefficiency: throw the page away, re-render on the server, ship a diff, for data the next person won't even share. Why run dynamic server caching if nothing is shared?
Adam Rackis breaks down how TanStack's RSC model differs: explicit, opt-in, not your primary data path (React Server Components in TanStack). Loaders + Query own data; RSCs keep expensive non-interactive trees off the client. In Next, RSC is the center of gravity. That gap is why auth-gated dashboards feel native in Start and upstream in Next.
So: static / SEO / CMS → Next. Thin public shell + thick private dashboard → Start, any day. Even after listing all those rough edges.




Top comments (0)