DEV Community

Cover image for What Next.js's instant navigation looks like in a multipage application
Alessandro Grosselle
Alessandro Grosselle

Posted on

What Next.js's instant navigation looks like in a multipage application

Next.js 16.3 ships a strong pitch for instant navigation, demonstrated in next-beats, a music player app built by Aurora Scharff.

Next.js team keep raising the bar with every release, and next-beats is a great example: source teaches you more than the announcement copy ever could.

But it's a single-page application, every navigation is a client-side React transition. That's exactly the case that never gets addressed: what happens to "instant navigation" when you're running a multi-page application (MPA) instead?

To find out, I forked next-beats into next-beats-mpa, live at next-beats-mpa.vercel.app, and, feature by feature, replaced the SPA-shaped pieces with their MPA-native equivalents.
This post walks through what changed, why, and shows the actual code on both sides.

Why "just migrate to a SPA" isn't always on the table

In a real MPA, clicking a link is a genuine HTTP request; the server returns a full HTML document. Two reasons make migrating away from that model to a SPA client-router unrealistic for a lot of real products:

  1. Third-party scripts you don't control. Analytics, ads, and social widgets frequently assume DOMContentLoaded fires, or that the browser does a full reload on every navigation. Going SPA means auditing (and possibly renegotiating) behavior with every one of those vendors, often impossible when the scripts are injected externally through a tag manager, outside your codebase.
  2. Memory leaks become your problem. An MPA reloads the page on every navigation, so listeners, setInterval timers, and components that don't unmount cleanly get wiped for free. In a SPA nothing reloads, so anything you forget to clean up accumulates for the lifetime of the session. The bugs don't disappear in an MPA, they're just masked by the full reload.

Given that framing, the real question is: which of Next.js 16.3's new instant-navigation features actually help an MPA, versus which ones only matter once you've already committed to a SPA client router?

View Transitions

next-beats uses React's <ViewTransition> to crossfade between routes. Concretely: components/ui/crossfade.tsx wraps app/(app)/playlist/page.tsx:

// components/ui/crossfade.tsx (next-beats)
import { ViewTransition } from 'react';

export function Crossfade({ children }: { children: React.ReactNode }) {
  return (
    <ViewTransition enter="auto" default="none">
      {children}
    </ViewTransition>
  );
}
Enter fullscreen mode Exit fullscreen mode

This works because React actually unmounts the old page and mounts the new one, client-side; there's no equivalent lifecycle hook during a real cross-document MPA navigation.

The MPA-native equivalent

For an MPA, the platform already has you covered with the Cross-document view transitions.
In next-beats-mpa, app/globals.css turns it on with a couple of lines:

/* app/globals.css (next-beats-mpa) */
@view-transition {
  navigation: auto;
}
Enter fullscreen mode Exit fullscreen mode

Use the mobile viewport if you want a more engaging page transition animation:

That alone gives the browser a cross-fade between full document loads, no JS required.

Hover-triggered prefetch

next-beats prefetches a link's data only once the pointer or focus reaches it, instead of eagerly prefetching everything in the viewport.
It's a genuinely nice feature, but it's built on next/link, which is designed around client-side navigation.

In an MPA you don't use next/link for cross-document navigation, so this prefetch mechanism simply isn't reachable.

The MPA-native equivalent

Two browser-native mechanisms get most of the same win without a client router.

First, BFCache: make sure pages qualify for it, so back/forward navigation restores from an in-memory snapshot instead of a fresh round-trip.

Second, and closer to a direct analogue of hover-prefetch, the Speculation Rules API: you declare rules that tell the browser to prefetch (or even prerender) a document ahead of the click, based on hover/pointerdown intent, and the browser does the fetching itself.

next-beats-mpa implements this as a single static script, components/speculation-rules.tsx:

// components/speculation-rules.tsx (next-beats-mpa)
const RULES = JSON.stringify({
  prefetch: [{ where: { selector_matches: '[data-prefetch]' }, eagerness: 'moderate' }],
  prerender: [{ where: { selector_matches: '[data-nav-link]' }, eagerness: 'conservative' }],
});

export function SpeculationRules() {
  return <script type="speculationrules" dangerouslySetInnerHTML={{ __html: RULES }} />;
}
Enter fullscreen mode Exit fullscreen mode

The trade-off:
Speculation Rules currently only ships in Chromium browsers (Chrome, Edge). Other browsers just ignore the unrecognized <script type="speculationrules">, so it degrades gracefully, but if your audience isn't Chromium-heavy, the win is partial.

use cache

Cache Components ('use cache', cacheTag, cacheLife) are genuinely useful on their own, and next-beats leans on them to cache query results with good DX.

This one's more of an architectural call than a technical rejection: if your backend is a set of external microservices, I'd rather push caching optimization to the backend/API layer than duplicate cache logic inside the Next.js app.

It's a legitimate tool for an MPA too, nothing about it depends on a client router, but if the goal is "make navigation instant" and you're fronting an external backend, I'd optimize at the source of truth first and treat use cache as a secondary lever, not the primary one.

Summary

The pattern across all of this is consistent: Next.js 16.3's instant-navigation story is built and demoed around a SPA-shaped client router.

The good news is the browser itself already ships MPA-native equivalents for almost everything:

SPA-flavored 16.3 feature MPA-native equivalent Where in next-beats-mpa
<ViewTransition> (React) @view-transition { navigation: auto; } + pageswap/pagereveal direction tagging (native CSS, cross-document) app/globals.css, components/view-transition-inline-script.tsx
Hover-triggered prefetch (next/link) BFCache tuning + Speculation Rules API components/speculation-rules.tsx, components/ui/prefetch-link.tsx
use cache Same tool, works in an MPA too — but optimize backend/API-level caching first if your BE is external/microservices n/a

The full diff between the two approaches is browsable directly: next-beats (SPA) vs. next-beats-mpa (MPA fork).

Top comments (1)

Collapse
 
101beardo profile image
Tarun Sharma

Great breakdown, the "masked not fixed" point on memory leaks is one people miss constantly when they treat MPA-to-SPA as a pure win. On the prerender rule for data-nav-link, does that ever cause double-fires on analytics or other side-effect scripts that run on page load, since the browser is rendering the destination page speculatively before the user actually commits to the click?