Back to blog

Next.js 16.3 and Instant Navigations: How to Get SPA Fluidity Without Giving Up the Server

Hello HaWkers, there is one criticism of Next.js you have probably heard (or made yourself): "navigation in an app with Server Components feels slow". You click the link, nothing happens for a moment, and only then does the next page show up. On a content site that goes unnoticed. In an application, it is annoying.

The Next.js team acknowledged this publicly and the answer came in 16.3, with a set of features called Instant Navigations. Do you know what changes in your next.config.ts to turn it on, and why Next.js stopped firing one prefetch per link? Let's look at it in practice.

The Real Problem: Two Gaps Between the Click and the Screen

Before talking about the solution, it is worth understanding why server-driven navigation takes time. There are two distinct gaps:

  1. The client needs to talk to the server. If latency is high, that is expensive no matter how fast your code is.
  2. The server needs to produce the response. If the query is slow, the user waits.

A client-driven app solves this because it already has the code for the next screen in the bundle. It shows a shell immediately and fetches the data afterwards. Next.js 16.3 attacks the two gaps separately, and that distinction is the key to understanding everything else.

Enabling Cache Components

All the new behavior sits behind a flag. First step:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

This flag is part of a bigger move Vercel has been making for about a year: taking Next.js back to its roots, being dynamic by default, with no implicit or hidden caching. The documentation already warns that cacheComponents will become the default in a future major version.

Stream, Cache or Block: You Choose

With the flag on, whenever a route awaits some data on the server, Next.js presents you with an explicit decision. There are three paths:

Stream with <Suspense> - the user sees a loading state right away, and the rest comes in through streaming.

import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <>
      <ProductHeader id={params.id} />
      <Suspense fallback={<InventorySkeleton />}>
        <InventoryStatus id={params.id} />
      </Suspense>
    </>
  );
}

Cache with 'use cache' - the user sees a UI that is already cached, reused across requests.

async function getFeaturedProducts() {
  'use cache';

  const products = await db.product.findMany({ where: { featured: true } });
  return products;
}

In both cases navigation becomes instant. But sometimes you want navigation to wait for the server. A blog, for example, may prefer never showing a loading shell on the post. That is what the third path is for:

// page.tsx or layout.tsx
export const instant = false;

This is the opposite of magic. The framework does not decide for you: it forces you to declare the intent of each route.

Instant Insights: Slow Navigation Becomes an Error

Here is the part that changes your day to day. In development, Next.js 16.3 treats non-instant navigation as an error, shown in a panel called Instant Insights. It points out exactly which routes do not navigate instantly and why.

In practice this flips the workflow. Before, navigation performance was something you measured later, if you remembered. Now it shows up while you are writing the code, the same way a type error does.

To avoid regressing after a refactor, a test helper for Playwright also arrived:

import { expect, test } from '@playwright/test';
import { instant } from '@next/playwright';

test('product title appears immediately', async ({ page }) => {
  await page.goto('/products/shoes');

  // Checks what is visible without waiting for the network
  await instant(page, async () => {
    await page.click('a[href="/products/hats"]');
    await expect(page.locator('h1')).toContainText('Cap');
    await expect(page.getByText('Checking inventory...')).toBeVisible();
  });

  await expect(page.getByText('12 in stock')).toBeVisible();
});

Notice what the test asserts: the title has to be visible before the network responds, and so does the inventory loading state. It is an assertion about perception, not about total time.

Partial Prefetching: One Shell per Route, Not One per Link

This is the most interesting change from an architecture standpoint.

Up to 16.2, Next.js fired a prefetch request for every link in the viewport. If you had a sidebar with twenty conversations, that meant twenty requests, all pointing to the same /chat/[id] route. Anyone who opened the Network tab in production saw that flood of calls and wondered what was going on.

16.3 borrows the trick from SPAs. Instead of fetching one page per link, Next.js now fetches a reusable shell per route, and caches that shell on the client. Twenty chat links become a single prefetch of the /chat/[id] shell.

Conceptually it is the same as route-based code splitting in a SPA: you download the structure once and reuse it.

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;

Since shells are reused across links, they also become the foundation for offline navigation. The team has already signaled that it intends to explore prefetched routes that stay navigable when the network drops for a few seconds.

When You Need More Than the Shell

Reducing prefetching has a cost: sometimes you want a specific piece of content to appear instantly, not just the skeleton. That is why <Link prefetch> still exists:

<Link href={`/chat/${id}`} prefetch={true}>
  {chat.title}
</Link>

But with one important difference: even then, Next.js will not render the whole route to the end. It renders as far as the content is available synchronously, known from the URL (params, searchParams) or marked with 'use cache'.

In other words, the "all or nothing" choice in prefetching is gone. The instant shell is the baseline, and <Link prefetch> combined with 'use cache' adds layers per link when it is worth it.

To inspect exactly what is being prefetched, DevTools gained a Navigation Inspector, which pauses each navigation at the shell and lets you see what would appear instantly. Keep in mind that real prefetching only happens in production.

A Migration Roadmap That Does Not Break Everything

Turning cacheComponents on in a large application and surfacing dozens of errors at once helps nobody. The path that makes sense is incremental:

1. Turn on the flag only and read the report. Do not change code yet. Instant Insights will list the routes that do not navigate instantly. That inventory is your prioritized backlog, and on its own it already tells you where your application is paying latency.

2. Classify each route before touching it. For every route on the list, decide which of the three paths it should follow. A read route with data that rarely changes calls for 'use cache'. A route with per-user data calls for <Suspense> around the slow part. A route that really needs the data before painting (checkout, payment confirmation) deserves export const instant = false, and that is not a defeat.

3. Start with the layout, not the page. Header, sidebar and navigation are usually the same across routes and are the best candidates for a reusable shell. Fixing the layout improves several routes at once.

4. Only then turn on partialPrefetching. With the routes already classified, shell-based prefetching has something to reuse. Turning it on before that just trades a flood of requests for empty shells.

5. Lock the result in with tests. Every route you made instant gets a test with the instant() helper. Without that, the first refactor undoes the work and nobody notices until the complaint shows up.

Worth mentioning that there is an official Skill published in the Next.js repository to drive Cache Components adoption with an AI agent, in case you prefer to delegate the initial sweep.

What Else Came in 16.3

Instant Navigations is the headline, but the release carries gains that reach applications that will not even adopt the new flags:

  • Memory in development: long next dev sessions use up to 90% less RAM
  • Repeated builds: artifacts that did not change are read from cache
  • Type checking: next build can use TypeScript 7 for type checking
  • Server rendering: up to 22% more requests served under load, using native Node.js streams
  • Versioned documentation for agents: AI tools read docs from the correct version with no configuration
  • Glob imports: importing multiple files through a new Turbopack API

The memory gain in dev alone already justifies upgrading in a large monorepo.

Is It Worth Adopting Now?

Two things weigh on the decision.

The first is that Vercel itself used this in production before shipping it. v0 has a lot of client-side interaction and its navigations had been underwhelming for a while. Instant Insights pointed out the problematic routes and the team fixed them one by one. The team promises to detail the patterns they adopted in a follow-up post.

The second is that there are still known issues in the Preview. The Instant Insights tooling has bugs in Safari, so in development it is better to use Chrome or Firefox. And, with Partial Prefetching on, accessing params inside a shell makes the route block without that being reported as an Instant Insight, although the Navigation Inspector and the instant() helper remain correct.

To try it out:

npm install next@preview

My read: if you maintain an application with a lot of internal navigation (dashboard, chat, admin panel), it is worth opening a branch and turning on cacheComponents just to see what Instant Insights flags. The report alone is already a valuable diagnosis, even if you adopt nothing right now. If your product is a content site, the urgency is much lower.

What This Says About the Direction of Next.js

There is a pattern here worth naming. Next.js spent years adding implicit caching and then spent the last few years removing it. Instant Navigations follow the same philosophy as the current phase: nothing happens behind your back, and the framework forces you to declare the intent of each route, whether Stream, Cache or Block.

It is a conscious trade-off. You write one extra line of configuration per route, and in exchange you know exactly what happens when someone clicks a link. After years of debugging caching nobody asked for, that sounds like a good deal.

If you want to understand the foundation everything here was built on, I recommend checking out another article: React Server Components in Production: The Complete Guide where I cover the architecture patterns that Instant Navigations assume you already know.

Let's go! 🦅

📚 Want to Keep Up With What Is Coming?

This article covered Next.js 16.3 and Instant Navigations, but the ecosystem shifts every week and not everything turns into an article here.

On X I share what I am testing, the behind the scenes of my projects and the news that shows up before it becomes a post.

Follow Me There

👉 Follow @jeffbruchado on X

💡 Daily content about development, career and the tools I actually use

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments