all-posts
Engineering#Next.js#React#Performance

Next.js 16 in production: what we learned shipping 12 apps

Turbopack is no longer optional, cache components change how you think about data, and partial prerendering quietly became the default. Our field notes from twelve production upgrades.

Abdenassar Amimi

Founder & Lead Engineer

September 18, 2026

8 min read

We have upgraded or launched twelve production applications on Next.js 16 over the last two quarters — a mix of e-commerce, healthcare, and internal platforms. This post is the field notes we wish someone had handed us before the first migration: what actually changed, what broke, and what is genuinely faster.

Turbopack is the default, and that is a big deal

The headline change is that Turbopack is now the default bundler for both dev and build. On our largest codebase (a retail platform with ~2,400 modules), cold dev-server start went from 14s to just under 3s, and hot reload latency dropped from ~900ms to ~120ms. Nobody measures this in a demo — you feel it after the hundredth change of the day.

The practical migration note: most webpack loaders and plugins have no Turbopack equivalent yet. If your build leans on custom loaders (SVG-as-component tricks are the usual offender), move them to plain imports or a post-processing step first. Every migration blocker we hit was loader-related, not framework-related.

Cache components make you declare your data strategy

The new caching model flips the mental model: nothing is cached unless you say so. That sounds scary in a changelog, but in practice it kills the entire genre of "why is this page showing stale data" bugs that dynamic rendering used to produce.

tsx
// Data is uncached by default in Next.js 16 —
// you opt in per await boundary.
export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const product = await getProduct(slug); // fresh every request
  const reviews = await getReviews(slug); // cacheable, rarely changes

  return (
    <>
      <ProductDetails product={product} />
      <Reviews data={reviews} />
    </>
  );
}
Per-boundary caching: fresh data by default, explicit caching where it pays.

Our rule of thumb after twelve apps: cache lists and shells, never cache cart, session, or anything behind auth. We measured a 38% reduction in origin requests on the commerce site after marking review lists and category pages as cacheable — with zero staleness complaints, because cart and checkout paths stayed fully dynamic.

The params gotcha that will bite every codebase

Dynamic route params, searchParams, cookies, and headers are now asynchronous. Every page that read params directly needs an await. Codemods handle most of it, but watch for the sneaky ones: params passed into client components as props, and destructuring inside utility functions that pages call.

  • →Grep for `params.` and `searchParams.` before you migrate — the codemod misses destructured calls in helper files.
  • →Type them as `Promise<{ ... }>` in your page props or the compiler will find them for you, slowly.
  • →Client components receiving params should receive plain values from the server instead — simpler than making them promise-aware.

Partial prerendering: the quiet win

The least flashy feature delivered the most measurable improvement. Pages with a static shell and small dynamic islands now stream the shell instantly while the dynamic parts resolve. On the healthcare portal, logged-in navigation felt instant even on 3G-simulated tests, because the app shell was prerendered and only the patient data streamed in.

The shell is the brand. If the frame of your app renders instantly, users forgive the data taking 300ms. If the frame itself takes 900ms, no spinner saves you.
— from our internal performance review template

What we would tell a team starting today

  • →Budget your JavaScript. 16 does not fix a 1.2MB client bundle; it just delivers it faster.
  • →Adopt Server Actions before the migration, not after — it shrinks your API surface and makes the new model natural.
  • →Put Core Web Vitals in CI, not a dashboard you check quarterly. Every one of our twelve apps has a perf budget gate on the main branch.
  • →Stay on the even-numbered release train for client work; let agencies like us find the odd-numbered bugs.

Is the upgrade worth it? For the six apps on Next.js 13/14 we touched, yes — measurably, in both developer velocity and shipped performance. For anything older, migrate straight to 16 in one hop; the intermediate versions only teach you deprecated patterns you will have to unlearn.

have-a-project-like-this

This is the kind of thinking we bring to client work. If it resonates, the discovery call is free.

Talk to us