Next.js vs TanStack Start on plain AWS
What actually changes when the same SSR app lands on the same AWS setup.
Put either framework behind CloudFront and, from the outside, it looks the same. The differences are in the details: where your code runs, how data gets changed, how fast new content goes live, and how much you have to maintain when something breaks.
Next.js via OpenNext
Does more out of the box (ISR, image optimization, middleware), but you need more AWS pieces and a community adapter to get there.
TanStack Start via Nitro
Fewer AWS pieces and clearer server/client boundaries. In return you get fewer built-in features, no official AWS target, and it was still a release candidate as of November 2025.
Everything here links to a source. The boxes marked Opinion or Inference are my own reading.
- 1. From merged PR to a page in the browser
- 2. What each framework adds
- 3. Where your code runs
- 4. How content gets fresh
- 5. Rendering strategy per route
- 6. Core Web Vitals
- 7. What you own when it breaks
- 8. Going deeper: Lambda and mutations
1. From merged PR to a page in the browser
Before comparing the frameworks, let's follow any SSR app from the moment a pull request is merged. Click through the steps or press Play.
2. What each framework adds
The setup above is the same for both frameworks. Pick one to see what extra it brings on the AWS side.
The server box is Lambda here. There is more about it at the end.
- SSG · SSR · ISR
- Image optimization
- Middleware
- Documented self-hosting
- Community adapter
- 6+ AWS resource types
- 25 CloudFront behaviors max
- No native-Windows build
- Small AWS footprint
- Host-agnostic server
- Optional Lambda streaming
- AWS not a listed target
- Community recipes only
- No ISR / image tool seen
- Release candidate (Nov 2025)
3. Where your code runs
This is the biggest difference between the two, and most of what comes next follows from it.
- Server Components by default (docs)
- Secrets stay server-side
- Less JS to the browser
'use client'pulls in its whole import graph- No React context in Server Components
- Props must be serializable
Watch out: 'use client' marks a boundary in the module graph, not on a single component. If you put it on a big file, everything that file imports goes to the browser. Keep it on small, leaf-level components.
- One mental model: it's just React + router
- Explicit server code:
createServerFn - Server handler never reaches client bundle
- Isomorphic by default → loaders run on the client too
- Module-level
process.envcan leak - Server Components: experimental, opt-in
Watch out: the docs are direct about it: "Route loaders are isomorphic". They run on the client too, so a secret or a database call inside a loader is a bug waiting to happen. Anything sensitive should live behind createServerFn or createServerOnlyFn.
4. How content gets fresh
This part decides how much infrastructure you run and how much you have to look after.
- Time-based: stale-while-revalidate
- On-demand: tags,
revalidatePath - No redeploy to update content
- Cache + tag state must be shared across instances
- HTML and RSC must be cached together
- More AWS parts to monitor
More detail in the docs on how revalidation works and in SST's list of resources. One thing to know: if a CDN caches the HTML and the RSC payload with different TTLs, users can see mismatched content when they navigate on the client.
I drew the pipeline above in a simplified order.
Of these, the HTML/RSC mismatch is the one I would expect to bite first.
- Prerender is plain static files
- Simplest cache story: S3 + CloudFront
- Navigation data via router loader cache
- No ISR in the prerender docs
- Dynamic routes prerender only if linked +
crawlLinks - New content = rebuild, or runtime fetch
If your catalog changes every hour, Start means rebuilding on a schedule or rendering live. That is fine for a lot of apps. But if editors expect to publish and see it live in seconds, you are better off paying for the Next/OpenNext machinery.
5. Rendering strategy per route
Whether a route is static or dynamic depends on which APIs it uses (docs). You don't declare it; Next works it out from your code.
Next works it out from what your code touches. That means less typing but more surprises: a harmless-looking cookies() call can silently turn a page dynamic.
ssr: trueloader on servercomponent → HTMLhydrate on client'data-only'loader on servercomponent on client onlyssr: falseloader on clientcomponent on clientYou declare it per route with selective SSR. A child route can only be stricter than its parent, never looser.
Start asks you to declare it per route. That takes more typing, but nothing changes behind your back.
6. Core Web Vitals
Google judges page experience with three numbers. A page counts as "good" when it hits the target for 75% of visits (web.dev).
What the framework does not decide
A slow first byte (TTFB) hurts LCP, but TTFB is not one of the three vitals, and most of it has little to do with the framework. It comes from how fast your API and database respond, how far the server is from the user, and whether CloudFront can cache the response (web.dev). A server-rendered page can even have a higher TTFB than a client-rendered one and still win on LCP. So don't choose a framework because of TTFB. Fix your slowest API call first.
Where the frameworks do differ
| What | Next.js | TanStack Start | Edge |
|---|---|---|---|
| LCP · images | Built-in image optimization (its own Lambda on AWS) | Plain <img> with the standard checklist below, served from S3 through CloudFront | Even if you follow the checklist; Next automates the sizing |
| LCP · cached HTML | Fully static pages are public, so CloudFront can cache them. ISR keeps them fresh. Dynamic pages are private, no-store and always reach the server (docs) | Prerendered pages are plain files, also cacheable (docs). No ISR, so fresh content means a rebuild | Even for static |
| LCP · streaming | Streams with Suspense | Streaming SSR (InfoQ) | Even if nothing on the path buffers |
| INP · JS in the browser | Server Components send no JS of their own; only 'use client' parts hydrate (docs) | Components are server-rendered and hydrated by default (docs) | Next, for content-heavy pages |
| CLS | Mostly your markup: image sizes, fonts, late-loading banners | Same | Even |
A good LCP image in TanStack Start
These practices come from web.dev's LCP guide and apply to any framework:
- Put the image in the server-rendered HTML
fetchpriority="high"on the hero only- Never
loading="lazy"on it srcsetfor the right size- AVIF or WebP
// routes/index.tsx
export const Route = createFileRoute('/')({
head: () => ({
links: [
{ rel: 'preload', as: 'image', href: '/img/hero-1200.avif', fetchPriority: 'high' },
],
}),
component: Home,
})
const Home = () => (
<img
src="/img/hero-1200.avif"
srcSet="/img/hero-600.avif 600w, /img/hero-1200.avif 1200w"
sizes="100vw"
width={1200}
height={630}
fetchPriority="high"
alt="Football pitch at sunset"
/>
)In Start you add tags to the page head through the route's head option (docs). The docs show a font preload, so the image version above is my own adaptation. The files sit in S3 and CloudFront caches them like any other asset. Since the <img> is already in the server-rendered HTML, the preload is optional; it matters more when the image is referenced from CSS or JavaScript.
One public test measured 116 KB of client JavaScript for TanStack Start against 193 KB for Next.js in the same dashboard app (LogRocket). It is a single app, so read it as "the framework runtime can matter", not as "one always wins". TanStack's own comparison page declines to name a winner on runtime size.
I didn't find any measured Core Web Vitals comparison between the two. Run Lighthouse and look at field data (CrUX) on your own app.
A content site with few interactive parts benefits most from Next's Server Components and image pipeline. A very interactive app ships most of its JS in either framework, so the gap gets smaller.
7. What you own when it breaks
- Adapter must keep up with Next releases
- Maintainers have limited capacity (OpenNext)
- 25 CloudFront behaviors per distribution
- Image Lambda memory + cold starts
- Cache/tag consistency bugs
The failure surface is wide, but a lot of people run this setup, so most errors are searchable. Pin your Next and OpenNext versions together and upgrade them as one unit.
- Release candidate as of Nov 2025 (InfoQ)
- Nitro Vite plugin "under active development"
- No first-party AWS runbook
- You build image + cache layers yourself
The failure surface is smaller, but fewer people have been there before. When something breaks you will be reading Nitro and Start source code more than Stack Overflow. Plan for that, and check the current release status before you commit.
8. Going deeper: Lambda and mutations
This part is optional. You can skip it and still make the call.
How a mutation reaches Lambda
Form posts and server functions end up as an HTTPS call that goes through CloudFront into your function. Each framework tends to fail in a different way here.
- Lambda = many instances
- Set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYor hit "Failed to find Server Action" - Rolling deploys: set
deploymentId
Source: the self-hosting guide, which describes this for multi-server setups.
Lambda runs many instances, so I am applying that guidance to it. The docs don't say it about Lambda specifically.
- Build swaps handler for RPC stub
- Validator (e.g. Zod) is first-class
- CSRF middleware by default
- Same-origin only: public APIs need server routes
- Define
src/start.ts→ you re-add CSRF - Auth belongs in each handler
From the server functions guide: beforeLoad "is not the data boundary".
Treat every server function like a public endpoint and do the authorization inside it. The same goes for Next Server Actions.
Gotchas that hit both
- POST requests through OAC need
x-amz-content-sha256 - Nothing on the path may buffer streamed responses
- With many Lambda instances, Next needs a shared cache and the same encryption key
Verdict
Choose Next.js if content has to go live in seconds, you want images and middleware handled for you, and you are fine owning the OpenNext pieces. Choose TanStack Start if you want the smallest AWS footprint, explicit server/client boundaries and portability, and you can build the extras yourself.
Prototype your hardest route in both before you commit, and measure Core Web Vitals on your own app.
Sources
- Next.js: Self-hosting
- Next.js: Server and Client Components
- Next.js: How revalidation works
- OpenNext for AWS
- SST: Nextjs component (AWS resources, limits)
- TanStack Start: Hosting
- TanStack Start: Execution model
- TanStack Start: Server functions
- TanStack Start: Selective SSR
- TanStack Start: Static prerendering
- TanStack: Start vs Next.js
- Nitro: AWS Lambda preset
- AWS: Restrict access to a Lambda function URL origin
- web.dev: Core Web Vitals
- web.dev: TTFB
- InfoQ: TanStack Start release candidate (Nov 2025)
- LogRocket: TanStack Start RSC vs Next.js RSC