Engineering8 min read

Next.js 15 Server Components in Production: What Actually Changed

By Niraj Jha ·

Co-Founder & CTO · Last updated

Key takeaways

  • The server/client boundary is one question per file: does this code need the browser? If not, it stays a Server Component.
  • 'use client' is a boundary, not a per-component flag - everything a client module imports gets pulled into the browser bundle.
  • Next.js 15 has four overlapping caches; most "stale data" reports are cache behaviour, not logic bugs.
  • Quarantine client-only libraries behind one wrapper you own instead of scattering the boundary across the app.
  • Treat a mutation without a matching revalidatePath/revalidateTag as an unfinished mutation.

React Server Components were sold to us as a clean separation: render on the server, ship less JavaScript, fetch data where it lives. In a tutorial, that story holds. In production - with auth, caching, third-party libraries, and a team that has to reason about where every line of code runs - the story gets more interesting.

We have shipped a dozen Next.js 15 apps on the App Router now. Here is what actually changed in how we build, what bit us, and the rules we settled on so we stop relearning the same lessons.

The mental model that finally stuck

The single biggest source of confusion is the server/client boundary. The breakthrough for our team was to stop thinking of it as "two kinds of components" and start thinking of it as one question per file: does this code need the browser?

If it needs state, effects, event handlers, or browser APIs, it is a Client Component and you put 'use client' at the top. Everything else stays on the server by default, where it can touch the database, read secrets, and ship zero JavaScript to the user.

The trap is treating 'use client' as a per-component decision. It is not - it is a boundary. Once a module is marked client, everything it imports is pulled into the client bundle too. We have seen a single careless 'use client' at the top of a layout drag an entire date-formatting library to the browser.

Push 'use client' as far down the tree as you can. A page should be a Server Component that renders a small interactive Client Component, not a client page that renders everything. The deeper the boundary, the less JavaScript ships.

What actually got better

This is not a cautionary tale. The wins are real, and they are the reason we did not roll back.

  • Data fetching lives next to the component that needs it. No more lifting a fetch to the top of the tree and prop-drilling it down. The component that shows the user's invoices fetches the invoices.
  • Secrets stay on the server. API keys, database URLs, and internal endpoints never cross the boundary. This removed a whole category of "oops, that was in the client bundle" leaks.
  • The default bundle is smaller. Marketing pages and content-heavy routes ship almost no JavaScript. Our Lighthouse scores went up without us optimising anything.

Where it bit us

Third-party libraries that assume the client

A large slice of the npm ecosystem was written before Server Components existed. Many libraries call window, use React Context, or rely on hooks at import time. Drop one into a Server Component and you get a cryptic build error.

Our rule: wrap client-only libraries in a thin 'use client' component that we own, and import that wrapper everywhere else. The boundary lives in one file we control, not scattered across the app.

Caching is now four overlapping systems

This is the part that surprised us most. Next.js 15 has the Request Memoization, the Data Cache, the Full Route Cache, and the Router Cache - and they interact. A response that looks stale is often a route cache hit, not a data bug.

CacheScopeWhen it surprised us
Request MemoizationSingle render passTwo fetches to the same URL silently deduped
Data CacheAcross requestsStale data after a write with no revalidation
Full Route CacheStatic routes at buildUpdated content not showing until redeploy
Router CacheClient-side navigationBack button showing old data for ~30s

Next.js 15 made fetches uncached by default, which removed the worst of the surprises - earlier versions cached aggressively and silently. But you still have to be deliberate. We now treat revalidatePath and revalidateTag after every mutation as non-negotiable, the same way we treat error handling.

Server Actions are great until they are your whole API

Server Actions let you mutate data from a form without writing an API route. For internal CRUD, they are a genuine productivity win. But they are POST requests under the hood, they are not free to call from a mobile client, and they do not give you a documented contract.

We use Server Actions for the app's own forms and mutations. The moment a second client appears - a mobile app, a partner integration - we add a typed API layer instead of stretching actions to cover it.

Server Actions are a feature of your web app, not an API strategy. If you can foresee external clients, do not let actions become the only way to write data. Retrofitting an API after the fact is far more expensive than planning for one.

The rules we ship by

After enough projects, the team converged on a short list that we now treat as defaults:

  1. Server by default, client by necessity. Add 'use client' only when the browser is genuinely required, and add it as deep in the tree as possible.
  2. One wrapper per client-only library. Quarantine the boundary in a file you own.
  3. Revalidate after every write. Treat a mutation without a matching revalidatePath/revalidateTag as an unfinished mutation.
  4. Keep Server Actions for your own app. Reach for a typed API the moment a second client is on the roadmap.
  5. Read the cache before debugging the data. Most "stale data" reports are cache behaviour, not logic bugs.

Was it worth it?

Yes - but with eyes open. The App Router and Server Components are not a free upgrade that makes everything faster. They are a different model with a real learning curve, and the caching layer in particular costs a team a few weeks of confusion before it clicks.

What you get on the other side is apps that ship less JavaScript, keep secrets where they belong, and put data fetching where it makes sense. For the kind of production software we build, that trade is worth making - as long as you respect the boundary and the cache instead of fighting them.

Frequently asked questions

When should a component use the "use client" directive?
Only when the browser is genuinely required - state, effects, event handlers, or browser APIs. Everything else stays a Server Component by default. Push 'use client' as deep in the tree as possible, because everything a client module imports is pulled into the client bundle.
Why does my Next.js 15 app show stale data?
Usually a cache hit, not a data bug. Next.js 15 has Request Memoization, the Data Cache, the Full Route Cache, and the Router Cache, and they interact. Check the cache layers before debugging your logic, and revalidate after every write.
Are Server Actions a replacement for an API?
No. Server Actions are a feature of your web app - great for your own forms and mutations. They are POST requests under the hood without a documented contract. The moment a second client appears (mobile, partner integration), add a typed API layer instead.
Are React Server Components worth the migration?
Yes, with eyes open. You get smaller default bundles, data fetching next to the component, and secrets that stay on the server. The cost is a real learning curve, especially around caching, that takes a team a few weeks to internalise.

Have a product to build?

Shunya ships production software - web, mobile, AI, and cloud - with one team that owns the whole stack from concept to launch. Tell us what you want to build.

Niraj Jha

Written by

Niraj Jha

Co-Founder & CTO

Co-Founder & CTO of Shunya Tech. Full-stack architect who sets the engineering culture and technical standards behind every product we ship - from database design to production delivery on Next.js, tRPC, and Prisma.

Last updated