# Makerkit v4: Instant Navigation with Next.js Cache Components

> Makerkit v4 rebuilds every route around Next.js 16.3 Cache Components, root params, and Partial Prefetching. Pages paint on click instead of after a query. Here is what you get, what it costs to upgrade, and the sharp edges we already hit for you.

*Published: 2026-08-04*
*Canonical: https://makerkit.dev/blog/changelog/makerkit-v4-instant-navigation*

---

Click a link and the page is already there.

Not a spinner that resolves quickly. Not a skeleton that flashes for 200ms. The header, the sidebar, the page title, the table chrome: all of it arrives in the first response, and only the parts that genuinely need fresh data fill in behind it.

That is Makerkit v4. Every route in all three Next.js kits is rebuilt around [Next.js 16.3 Cache Components](/docs/next-supabase-turbo/installation/v4-migration), so your app paints on click instead of after a database query.

**TL;DR**

- **See it before you read it:** [Modern Next.js](/modern-nextjs) runs this in your browser. Race the two routes, move a Suspense boundary, dial a cache profile, and watch what paints.
- **What your users get:** pages appear on click. Nothing jumps around as data arrives, because the layout was already on screen holding its shape.
- **What you get:** marketing pages that are safe on any CDN, blog and docs prerendered at build, and 69 of 72 routes prerendering something.
- **What it costs:** ten tagged steps, a few hours, no rewrite. Every tag is a working state you can merge and deploy on its own.
- **Where it ships:** **v4** on the Next.js Supabase kit, **v2** on the Next.js Drizzle and Prisma kits. Same work, different version numbers.
- **Stack:** Next.js 16.3, React 19, TypeScript 7, oxlint, oxfmt, Tailwind 4, Base UI.

## Your app was slow for a reason that was never your fault

The pattern that makes a Next.js SaaS feel slow is the pattern the framework teaches you. Open the members page, the server runs a query, and until that query returns the browser has nothing to render. The header does not depend on that query. It waits for it anyway.

Here is the entire fix:

```diff
- async function Page({ params }) {
-   const members = await loadMembers();   // blocks the entire page
-   return <PageBody><PageHeader /><MembersTable members={members} /></PageBody>;
- }

+ function Page({ params }) {
+   return (
+     <PageBody>
+       <PageHeader />
+       <Suspense fallback={<TableSkeleton />}>
+         <MembersTable params={params} />     {/* only this waits */}
+       </Suspense>
+     </PageBody>
+   );
+ }
```

Look at the header. In the first version it cannot render until `loadMembers()` comes back, even though it never touches the result. In the second it renders immediately and the table arrives when it arrives.

This is easier to watch than to read about. We built [an interactive walkthrough of the Next.js 16 rendering model](/modern-nextjs) that renders both of those routes side by side, so you can [race them](/modern-nextjs#demo) and then [move the boundary yourself](/modern-nextjs#suspense) and see exactly what lands in the first response.

That is not a new idea. What is new is that Next.js 16 can now prerender the static part of that page at build time and stream the rest per request, in the same route, which was impossible when prerendering was all or nothing.

Applying it to one page is an afternoon. Applying it to every page in a SaaS kit, including auth, billing, admin, blog, docs, and a multi-tenant dashboard, is what v4 is.

## The number that matters is zero

Makerkit v4 ships zero routes that opt out of instant navigation, which is both the claim worth checking and the one most starter kits cannot make.

Next.js gives you `export const instant = false` to mark a route as allowed to block. In your own app that is a reasonable escape hatch for the one page with a stubborn dependency. In a starter kit it is something else: every opt-out hands every customer a route that is quietly slow, forever, in a file they did not write and will not think to audit. The order of the ten migration steps exists specifically so the flag turns on with none.

The build tells the same story. Before the migration, one route in the Drizzle kit prerendered anything and 37 were fully dynamic. After it, 15 routes are fully static, 54 ship a prerendered shell with data streaming in, and the 3 that remain dynamic are API route handlers that only ever run per request. The Supabase kit lands in the same place from a different starting point. The [full route breakdown](/modern-nextjs#proof) is on the Modern Next.js page, split by what each of the 72 routes actually prerenders.

## Your marketing pages are now safe on any CDN

Caching a Makerkit marketing page no longer risks leaking a signed-in user's data, because the server HTML no longer contains any.

In v3 those pages were `force-dynamic`. The layout awaited the session to decide whether to render "Sign in" or "Dashboard", so user data ended up in the response. A shared cache could then hand one visitor another visitor's header. The `force-dynamic` flag kept that out of caches and cost you the cache.

v4 moves that decision to the client. The server HTML is identical for everyone, so the page is safe on a CDN regardless of how the CDN is configured. That is a stronger guarantee than a flag, because a flag is a configuration you can get wrong and an empty response body cannot leak.

We verified it with a real session rather than a grep, because a grep that finds nothing proves nothing. The check needs two controls first: prove the session cookie actually works, meaning the authenticated route redirects without it and returns 200 with it, and prove the scan can detect a leak, meaning it finds your email on an authenticated page. Only then does a clean result mean anything. Ours came back byte-identical between anonymous and authenticated requests for the same URL.

Re-run that check whenever you touch your marketing layout. Putting a server-side session read back in is a one-line change with a very bad failure mode.

## What you are actually buying: the sharp edges we already hit

We hit every one of these porting three kits, and most of them failed in ways that pointed us at the wrong file. Here they are as a checklist, and the [shorter symptom-first version](/modern-nextjs#sharp-edges) sits alongside the demos if you want to hand someone the list without the release notes around it.

**`next build` stays green when the migration silently fails.** A route that blocks on runtime data is valid, it just is not instant. The build passes. The only reliable signal is the dev overlay, per route, in a browser. We audited every route that way after the port and found ones a clean `next build --debug-prerender` never flagged.

**Two insight classes surface differently, and one of them hides from you.** Uncached data outside a boundary shows up on a direct page load. `params` or `searchParams` outside a boundary shows up **only on a client-side navigation**. A page that awaits `searchParams` at the top loads perfectly on a direct hit and reports nothing at all. Click a `<Link>` to it and the overlay flags it. Auditing by visiting URLs will not find these, so grep:

```bash
grep -rn "await searchParams\|await props.searchParams" \
  apps/web/app packages/*/src --include="page.tsx"
```

**`<Suspense>` does not fix an unstable value.** `Date.now()`, `new Date()`, and `Math.random()` fail differently from uncached data. Uncached data stops the prerender gracefully. An unstable value errors, because the same render would produce a different answer next time. The fix depends on what the value is for:

| The value is | Fix |
|---|---|
| Per-request | `await connection()` before you read it |
| Stable for a while | `use cache` with `cacheLife` |
| Browser-only | `'use client'` |
| Telemetry | `performance.now()` |

Await `connection()` properly. It stalls a prerender by returning a promise that never resolves, so `void connection()` does nothing.

**Never wrap a `use cache` function in React's `cache()`.** They do not compose. The build fails with `Invalid value used as weak map key`, reported against `generateMetadata` rather than the loader that caused it, which sends you hunting in the wrong file. The wrapper is redundant anyway, since `use cache` dedupes across requests and not just within one.

**Your Playwright suite will break, and the failure will lie to you.** Cache Components enables React's `<Activity>`, so the route you navigate away from stays mounted and hidden instead of unmounting. Every `data-testid` matches twice after a navigation. `page.locator()` and `getByTestId()` are strict and tell you so. `page.click()` and `page.fill()` are not: they take the first match, usually the invisible stale one, then wait for it to become visible until the test times out. No error, no clue, two minutes gone. Scope selectors and containers with `:visible`.

**`<Activity>` also keeps your dialogs open.** A dialog whose submit action ends in `redirect()` used to be destroyed by the navigation, so nothing ever needed to close it. Now it reappears, backdrop and all, when the user returns to that route. Close it explicitly on navigation. Do not reach for a generic effect cleanup: cleanup also runs on ordinary unmount and on StrictMode's double invoke, so conditionally mounted dialogs unmount the instant they open. We shipped that, then reverted it.

**A stale `.next` looks exactly like a code regression.** Switching between tags without clearing it gave us `/api/auth/*` returning 404 across an entire suite. Nothing was wrong with the code. We lost an hour to that before anyone said the obvious thing out loud, so: `rm -rf .next` after any checkout.

## Why Makerkit is the most modern Next.js boilerplate you can buy

Most starter kits ship the framework's default rendering model and stop. Makerkit v4 ships the model Next.js 16 was designed around, applied to every route, validated with the opt-out count at zero.

Cache Components, Partial Prerendering, `next/root-params`, and Partial Prefetching are all on, and every route passes validation with them on. "Built with Next.js 16" tells you what is in someone's `package.json`. This tells you what the routes actually do when a user clicks something.

The toolchain underneath is native. Linting runs on oxlint and formatting on oxfmt, both from the Oxc toolchain, and type checking runs on TypeScript 7, the Go-ported native compiler. `pnpm run healthcheck` chains all three into one command. A fast deterministic feedback loop matters most when an AI agent is verifying its own work before handing it back to you.

The other thing we build for now is coding agents. Fourteen `AGENTS.md` files ship in the repo, one per package, with `CLAUDE.md` and `GEMINI.md` pointing at the same source so Claude Code, Cursor, Windsurf, and Gemini CLI all read one set of rules. A bundled MCP server exposes Makerkit-specific tools to your agent. The v4 rules include the Cache Components non-negotiables, so an agent working in your codebase knows not to `await searchParams` at the top of a page before it writes the line.

The upgrade itself is ten tags, each a working state. Your app builds, runs, and deploys after every one. Merge step 4, ship it, come back to step 5 next week. The migration guide names what breaks along the way: the workspace hooks that now suspend, the auth gating that moved into the proxy, the tests that will fail and why the failure is misleading. That guide ships inside the repo as well as on this site.

## Upgrading is ten steps, and you can stop after any of them

Each step below is one tag you can merge, verify, and deploy on its own.

1. **Next.js 16.3.** The dependency floor for everything below.
2. **`app/[locale]/layout.tsx` becomes the real root layout.** v3 shipped a pass-through `app/layout.tsx` whose only job was to give `app/not-found.tsx` a layout. That pass-through counted as the root layout, so `[locale]` sat one level below it and never qualified as a root param.
3. **The locale resolves via `next/root-params`.** next-intl's `requestLocale` is a lazy getter that calls `headers()` the moment it is read. That single read is what made every route request-bound in v3.
4. **Loading states scale to the route.** Five `loading.tsx` files all rendered the same full-page spinner. Each now gets treatment proportional to how predictable its shape is, with shared blocks that step 6 reuses as Suspense fallbacks.
5. **Marketing auth moves to the client.** This is the CDN change above.
6. **`<Suspense>` boundaries across the app.** Layouts pass promises down unawaited and render their shell and `children` immediately. This is where most of the work lands.
7. **Cache Components on.** Zero opt-outs.
8. **Blog, changelog, and documentation prerender at build.** `generateStaticParams` plus `use cache`, in one pass.
9. **Partial Prefetching on.** Every `<Link>` loads the shared App Shell for its destination.
10. **A visual refresh.** Purely cosmetic and entirely optional.

Next.js 16.3 is a hard floor, and skipping it fails quietly. `next typegen` on an older version does not error, it emits no root params file at all, so you get a missing type instead of a message telling you why. If step 3 looks broken, check your resolved Next version before you touch anything else.

## Quick Recommendation

**Makerkit v4 is best for:**

- Teams shipping a Next.js SaaS where users spend all day clicking between routes
- Anyone starting a new Next.js 16 project who wants Cache Components applied correctly from day one
- Existing customers on v3 who can spend a few hours merging ten tagged steps

**Skip the upgrade for now if:**

- You are pinned below Next.js 16.3 by another dependency and cannot move yet
- You are mid-launch and have no time to run your e2e suite against `<Activity>` behavior
- You have heavily customized the app directory and cannot spend the time on steps 6 and 7

**Our pick:** upgrade incrementally. Merge steps 1 through 6, which improve your app under the old rendering model on their own, then take 7 through 9 when you have time to walk every route in the dev overlay.

## Which kit, which version

The same work ships across three Next.js kits under two version numbers, because the kits were on different majors when it landed.

| Kit | Version | Branch | Tags |
|---|---|---|---|
| Next.js Supabase | v4 | `v4` | `v4-step/*` |
| Next.js Drizzle | v2 | `v2` | `v2-step/*` |
| Next.js Prisma | v2 | `v2` | `v2-step/*` |

Drizzle and Prisma are architecturally identical here and differ only in `packages/database`. The Supabase kit is a deeper variant, with Supabase Auth and RLS-enforced authorization, so a few steps differ in detail. Each kit ships its own migration guide with its own tag names and version numbers.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "What are Next.js Cache Components?", "answer": "Cache Components is the Next.js 16 rendering model that makes Partial Prerendering the default. Each route ships a static HTML shell that serves immediately, and dynamic regions stream into it at request time. You choose what to cache with the `use cache` directive rather than marking whole routes static or dynamic."},
     {"question": "Do I have to upgrade to v4 all at once?", "answer": "No. v4 ships as ten tagged steps and every tag is a working state, so your app builds, runs, and deploys after each one. Merge a step, verify it, ship it, and come back to the next one later. Steps 1 through 6 improve your app even under the old rendering model."},
     {"question": "Does v4 require Next.js 16.3?", "answer": "Yes, and it is a hard floor. `next/root-params` does not exist before 16.3, and Step 3 depends on it. `cacheComponents` itself has been available since 16.0, but root params set the floor for the migration as a whole. Step 1 bumps it for you. If you pin Next yourself, update your pin first, because an older version emits no root params file rather than failing with an error."},
     {"question": "Will the upgrade break my Playwright tests?", "answer": "Probably, and the failure is misleading. Cache Components enables React's `<Activity>`, so the previous route stays mounted and hidden and every selector matches twice after a navigation. `page.click` and `page.fill` take the first match, often the invisible one, and hang until timeout with no error. Scope selectors with `:visible`."},
     {"question": "Is `export const instant = false` acceptable?", "answer": "It is a legitimate escape hatch for your own routes, but every one you add is a to-do item: it marks a route that is not navigating instantly. Treat the count as something you work down over time. The kit itself ships zero, because the step order is designed so the flag turns on cleanly."},
     {"question": "Which Makerkit kits include this work?", "answer": "The Next.js Supabase kit ships it as v4, and the Next.js Drizzle and Prisma kits ship it as v2. The version numbers differ only because the kits were on different majors. The rendering work is the same."}
   ]
/%}

## Next steps

If you want the rendering model itself rather than the release, start with [Modern Next.js](/modern-nextjs): Suspense, Partial Prerendering, and Cache Components explained one at a time, each with a demo you can drive.

Read the [v4 migration guide](/docs/next-supabase-turbo/installation/v4-migration) for the step by step upgrade, including what to check after each tag. New to the kit? Start with the [Next.js Supabase installation docs](/docs/next-supabase-turbo/installation/introduction), or the [Drizzle](/docs/nextjs-drizzle/installation/overview) and [Prisma](/docs/nextjs-prisma/installation/overview) variants. For how we pick the stacks we ship, see [the best Next.js SaaS boilerplate](/blog/saas/best-nextjs-saas-boilerplate).
