DEV Community

Cover image for I Turned On Cache Components in Next.js 16.3. It Refused to Build My Simplest Page.
Shubhra Pokhariya
Shubhra Pokhariya

Posted on

I Turned On Cache Components in Next.js 16.3. It Refused to Build My Simplest Page.

I didn't want to write another "what's new in Next.js 16.3" post. Enough of those exist. I wanted to know what actually happens when you flip the flags on a real project, so I scaffolded a fresh app, turned on cacheComponents and partialPrefetching, and tried to build the simplest possible dynamic page.

It didn't build.

The setup

Nothing fancy. Fresh create-next-app, Next.js 16.3.1, two flags in next.config.ts:

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};
Enter fullscreen mode Exit fullscreen mode

Then a product page. No database, no external API, not even a real fetch, just a fake delay and the route's own params:

async function getProduct(id: string) {
  await new Promise((resolve) => setTimeout(resolve, 300));
  return { id, name: `Product ${id}`, price: 42 };
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);

  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold">{product.name}</h1>
      <p>${product.price}</p>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

That's about as plain as a dynamic route gets. I expected next build to shrug and move on.

What actually happened

Error: Route "/products/[id]": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route
Enter fullscreen mode Exit fullscreen mode

Next.js 16.3 build error for params route showing the uncached-or-runtime-data error with stream/cache/block options

Full build failure. Exit code 1. For a page whose only "dynamic" behavior is reading a param that's obviously going to be there.

My first reaction was that this felt aggressive. My second reaction, after rereading it, was that this is the whole point of the release and I'd missed it by only skimming the changelog. Cache Components in 16.3 doesn't treat an unguarded dynamic access as a warning. It treats it as a decision you have to make, explicitly, before it'll ship your build. Stream it, cache it, or mark it blocking on purpose. No silent fourth option where it just works and you find out it was never instant three months from now.

The fix, and what it actually buys you

I took the option the error suggested first: wrap the part that awaits params in <Suspense>.

import { Suspense } from "react";

async function getProduct(id: string) {
  await new Promise((resolve) => setTimeout(resolve, 300));
  return { id, name: `Product ${id}`, price: 42 };
}

async function ProductDetails({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);
  return (
    <>
      <h1 className="text-2xl font-bold">{product.name}</h1>
      <p>${product.price}</p>
    </>
  );
}

export default function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  return (
    <main className="p-8">
      <Suspense fallback={<p>Loading product...</p>}>
        <ProductDetails params={params} />
      </Suspense>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Ran the build again:

Route (app)
┌ ○ /
├ ○ /_not-found
└   /products/[id]
  └ ◐ /products/[id]

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
Enter fullscreen mode Exit fullscreen mode

Next.js 16.3 successful build output showing Partial Prerender symbol ◐ for /products/[id]

That next to my product route is the actual feature. Next.js pulled out everything around the Suspense boundary, the shell, and prerendered it. The part that actually depends on which product you clicked streams in behind it. That's Cache Components' Partial Prerendering doing exactly what it's supposed to, a real route in a real build output, not a description from the release notes. It's also the same shell mechanism that Partial Prefetching, the client-side navigation feature 16.3 adds on top of this, depends on existing in the first place.

Browser rendering the product page at localhost:3000/products/123 showing Product 123 and $42

What this told me that the changelog didn't

Reading about Partial Prefetching, "a reusable shell per route, cached on the client," sounds like a nice-to-have you'd adopt when you get around to it. Hitting the build error told me something different: on a 16.3 app with Cache Components on, there's no version of "get around to it." Every uncached or runtime access Next.js finds during prerendering needs an answer, stream, cache, or block, the moment you turn the flag on. It's not gradual. It's a build gate.

That's a bigger adoption cost than "add two lines to your config" makes it sound, and I think that's exactly why Next.js ships next-cache-components-adoption as a first-party agent Skill now instead of leaving you to hunt down every unguarded params and cookies() call by hand across a real app. On a five-file test project this took me two minutes. On a real production route tree, I wouldn't want to do it without that tool, or without a very good sense of where every Suspense boundary in the app already is.

Where I'd actually start

If you're evaluating this for a real app: don't turn the flag on for the whole project and see what catches fire. Pick one route, ideally your simplest dynamic one, and watch what the build tells you it needs. It will tell you exactly what to do next. That part, at least, the error message gets right.

I wrote a longer breakdown of the full 16.3 release, what upgrades automatically with zero config changes, what Instant Navigations and Partial Prefetching actually change, and what shipped for AI agents, on my website if you want the whole picture before you decide whether to flip these flags on something real.

Has anyone else run the migration on an actual production route tree yet? I'd like to know what the first pass looks like on a real app.

Top comments (0)