·Updated

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, so pages render on click instead of after a database query. What changed, how the nine-step upgrade works, and the migration problems we hit.

Makerkit v4 rebuilds every route in our three Next.js kits around Next.js 16.3 Cache Components, so a page renders when the user clicks a link, not after a database query returns. The header, sidebar, page title and table layout arrive in the first response. Only the regions that need per-request data stream in afterwards.

TL;DR

  • Try it first: Modern Next.js runs the rendering model in your browser. You can race two versions of the same route, move a Suspense boundary, and change a cache profile to see what renders first.
  • For your users: pages appear on click, and the layout does not shift as data arrives because it was rendered before the data.
  • For you: marketing pages that are safe to cache on any CDN, blog and docs prerendered at build time, and 69 of 72 routes prerendering at least part of the page.
  • Upgrade cost: nine tagged steps and about 2-4 hours on a lightly customized app (1-2 days if you rewrote many pages). Every tag builds, runs and deploys on its own.
  • Versions: v4 of the Next.js Supabase kit, v2 of the Next.js Drizzle and Prisma kits. The work is the same; the version numbers differ.
  • Stack: Next.js 16.3, React 19, TypeScript 7, oxlint, oxfmt, Tailwind CSS 4, Base UI.

Why Next.js SaaS pages feel slow

Most Next.js SaaS pages feel slow because the page component awaits its data before returning anything, which is the pattern most tutorials teach. On a members page, the server runs a query and the browser receives nothing until it returns. The header does not use that query's result, but it waits for it anyway.

The fix is to move the await into a component wrapped in <Suspense>:

- 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>
+ );
+ }

In the first version, <PageHeader /> cannot render until loadMembers() resolves, even though it never reads the result. In the second, the header renders immediately and the table renders when its data is ready.

The Modern Next.js walkthrough renders both versions side by side, so you can race them and move the boundary yourself to see what lands in the first response.

Suspense itself is not new. What Next.js 16 adds with Cache Components is the ability to prerender the static part of a route at build time and stream the rest per request, in the same route. Before, a route was either fully prerendered or fully dynamic. (Our Next.js 16 overview covers the rest of that release.)

Restructuring one page this way takes an afternoon. v4 applies it to every page in the kit: auth, billing, admin, blog, docs and the multi-tenant dashboard.

Zero routes opt out of instant navigation

Makerkit v4 ships with no route marked export const instant = false. That is the number to check in any Next.js 16 starter kit.

instant = false tells Next.js a route is allowed to block navigation. In your own app it is a reasonable exception for one page with a dependency you cannot cache yet. In a starter kit, each opt-out becomes a permanently slow route in every customer's app, in a file they did not write and are unlikely to audit. We ordered the migration steps so that when Cache Components turns on, no route needs the flag.

The build output shows the result. 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 remaining 3 are API route handlers, which run per request by design. The Supabase kit reaches the same result from a different starting point. The route breakdown on the Modern Next.js page lists what each of the 72 routes prerenders.

Marketing pages are safe to cache on a CDN

A Makerkit v4 marketing page can be cached by any CDN without leaking a signed-in user's data, because the server HTML contains no user data.

In v3 those pages used force-dynamic. The layout read the session to choose between "Sign in" and "Dashboard", so user data was in the response, and a shared cache could serve one visitor's header to another. force-dynamic kept the pages out of caches, which also meant they were never cached.

v4 makes that choice on the client. The server HTML is identical for every visitor, so it is safe to cache regardless of CDN configuration. A cache flag can be misconfigured; a response that contains no user data has nothing to leak.

We tested this with a real session, not a grep, because a grep that finds nothing does not prove there is nothing to find. The test needs two controls before its result means anything:

  1. The session cookie works: an authenticated route redirects without it and returns 200 with it.
  2. The scan can detect a leak: it finds your email on an authenticated page.

With both controls passing, anonymous and authenticated responses for the same marketing URL were byte-identical.

Run the same test whenever you change the marketing layout. Adding a server-side session read back is a one-line change, and the result is one user's data served to another.

Cache Components migration problems we hit

We hit each of these while porting three kits, and most of them produced errors that pointed at the wrong file. The Modern Next.js page has a shorter, symptom-first version of this list.

next build passes when a route is not instant. A route that blocks on runtime data is still valid, so the build succeeds. The reliable check is the Next.js dev overlay, route by route, in a browser. We audited every route that way after the port and found blocking routes that next build --debug-prerender did not report.

Blocking params and searchParams only show up on client-side navigation. Uncached data outside a Suspense boundary is reported on a direct page load. params or searchParams awaited outside a boundary is reported only when you navigate to the page with a <Link>. A page that awaits searchParams at the top loads without warnings when you open its URL directly. Visiting URLs will not find these, so search for them:

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

<Suspense> does not fix non-deterministic values. Date.now(), new Date() and Math.random() fail differently from uncached data. Uncached data stops the prerender at the boundary. A non-deterministic value throws an error, because rendering twice would produce different output. The fix depends on what the value is for:

The value isFix
Per-requestawait connection() before you read it
Stable for a whileuse cache with cacheLife
Browser-only'use client'
Telemetryperformance.now()

connection() must be awaited. It stops the prerender by returning a promise that never resolves during prerendering, so void connection() has no effect.

Do not wrap a use cache function in React's cache(). The two do not compose. The build fails with Invalid value used as weak map key, and the error is reported against generateMetadata instead of the loader that caused it. The wrapper is also unnecessary: use cache deduplicates across requests, not only within one.

Playwright tests fail with a misleading timeout. Cache Components enables React's <Activity>, so the route you navigate away from stays mounted but hidden. After a navigation, every data-testid matches twice. page.locator() and getByTestId() are strict and report the duplicate. page.click() and page.fill() are not: they take the first match, usually the hidden element from the previous route, and wait for it to become visible until the test times out after two minutes, with no error message. Add :visible to selectors and containers.

<Activity> keeps dialogs open. A dialog whose submit action ends in redirect() used to be unmounted by the navigation, so nothing had to close it. Now it is still open, backdrop included, when the user returns to that route. Close it explicitly on navigation. Do not close it in a generic effect cleanup: cleanup also runs on normal unmount and on React StrictMode's double invocation, so conditionally mounted dialogs close as soon as they open. We shipped that version and then reverted it.

A stale .next directory looks like a code regression. After switching between migration tags without clearing it, /api/auth/* returned 404 across the whole test suite, with no problem in the code. It cost us an hour. Run rm -rf .next after every checkout.

What else ships in Makerkit v4

Beyond the rendering changes, v4 updates the toolchain and the files that coding agents read.

Rendering. Cache Components, Partial Prerendering, next/root-params and Partial Prefetching are all enabled, and every route passes the dev overlay's validation with them on. Many starter kits list "Built with Next.js 16" because it is in package.json, while their routes still render the Next.js 15 way. In v4 the routes use the Next.js 16 model.

Toolchain. Linting runs on oxlint and formatting on oxfmt, both from the Oxc project. Type checking runs on TypeScript 7, the native compiler ported to Go. pnpm run healthcheck runs all three. Fast checks matter most when a coding agent runs them to verify its own changes before handing them back.

Agent instructions. The repo includes fourteen AGENTS.md files, one per package, and the CLAUDE.md and GEMINI.md files point to them, so Claude Code, Cursor, Windsurf and Gemini CLI read the same rules. A bundled MCP server gives your agent Makerkit-specific tools. The v4 rules include the Cache Components requirements, so an agent working in your codebase knows not to await searchParams at the top of a page. See AI agents for how the files are organized.

Migration guide. The guide lists what breaks along the way: workspace hooks that now suspend, auth gating that moved into the proxy, and the Playwright failures above. It ships inside the repo as well as on this site, so you can point an agent at it.

How to upgrade to Makerkit v4

The upgrade is nine tagged steps, and your app builds, runs and deploys after each one. You can merge step 3, ship it, and come back to step 4 next week.

Prerequisite: Next.js 16.3 or later. The bump is not one of the steps; it already landed on the v3 branch as a regular dependency update, so pulling the latest v3 gives you it. If you pin Next.js yourself, update the pin first. An older version fails without an error: next typegen does not emit the root params type file at all, so step 2 fails with a missing type instead of a message explaining why. If step 2 looks broken, check the resolved Next.js version first.

  1. app/[locale]/layout.tsx becomes the root layout. v3 shipped a pass-through app/layout.tsx whose only job was to give app/not-found.tsx a layout. Next.js treated that file as the root layout, so [locale] sat one level below it and did not qualify as a root param.
  2. The locale resolves via next/root-params. next-intl's requestLocale is a lazy getter that calls headers() when read. That one call made every route request-bound in v3.
  3. Loading states match the route. In v3, five loading.tsx files rendered the same full-page spinner. Each now renders a skeleton shaped like its route, built from shared blocks that step 5 reuses as Suspense fallbacks.
  4. Marketing auth moves to the client. This is the CDN change described above.
  5. <Suspense> boundaries across the app. Layouts pass promises down without awaiting them and render their shell and children immediately. Most of the migration work is in this step.
  6. Cache Components on. No route opts out.
  7. Blog, changelog and documentation prerender at build. generateStaticParams plus use cache, in one pass.
  8. Partial Prefetching on. Every <Link> prefetches the shared App Shell for its destination instead of a full per-link prefetch.
  9. Visual refresh. Cosmetic only, and optional.

Should you upgrade now?

Upgrade incrementally. Merge steps 1 through 5, which improve your app under the old rendering model on their own, then merge 6 through 8 when you have time to check every route in the dev overlay.

Makerkit v4 fits:

  • Teams building a Next.js SaaS where users click between routes all day
  • New Next.js 16 projects that want Cache Components set up correctly from the start
  • Existing v3 customers who can spend a few hours merging nine tagged steps

Wait if:

  • Another dependency pins you below Next.js 16.3
  • You are mid-launch and cannot run your e2e suite against <Activity> behavior yet
  • You have heavily customized the app directory and cannot yet spend the time on steps 5 and 6

Which kit, which version

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

KitVersionBranchTags
Next.js Supabasev4v4v4-step/*
Next.js Drizzlev2v2v2-step/*
Next.js Prismav2v2v2-step/*

The Drizzle and Prisma kits are identical for this migration apart from packages/database. The Supabase kit uses Supabase Auth and RLS-enforced authorization, so a few steps differ in detail. Each kit has its own migration guide with its own tag names and version numbers.

Frequently Asked Questions

What are Next.js Cache Components?
Cache Components is the Next.js 16 rendering model that makes Partial Prerendering the default. Each route ships a static HTML shell that is served immediately, and dynamic regions stream into it at request time. You choose what to cache with the `use cache` directive instead of marking whole routes static or dynamic.
Do I have to upgrade to v4 all at once?
No. v4 ships as nine 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 5 improve your app even under the old rendering model.
Does v4 require Next.js 16.3?
Yes. `next/root-params` does not exist before 16.3, and Step 2 depends on it. The bump already landed on the v3 branch, so pulling the latest v3 before you start gives you it. If you pin Next.js yourself, update your pin first, because an older version emits no root params type file instead of failing with an error.
Will the upgrade break my Playwright tests?
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 hidden one, and wait until the test times out with no error. Add `:visible` to your selectors.
Is `export const instant = false` acceptable?
It is a legitimate option for your own routes, but each one marks a route that does not navigate instantly. Keep a list and reduce it over time. The kit itself ships none, because the step order lets Cache Components turn on without any.
Which Makerkit kits include this work?
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 major versions. The rendering work is the same.

Next steps

For the rendering model itself, start with Modern Next.js: Suspense, Partial Prerendering and Cache Components explained one at a time, each with a demo.

For the upgrade, read the v4 migration guide, which lists what to check after each tag. New to the kit? Start with the Next.js Supabase installation docs, or the Drizzle and Prisma variants. For how we choose the stacks we ship, see the best Next.js SaaS boilerplate.