Every navigation Next.js performs is either a soft navigation (a client-side swap of just the changed segment, layouts preserved) or a hard one (a full document reload, everything lost). Prefetching and streaming exist entirely to make more of your navigations soft.
1. What's the key difference between a soft navigation and a hard navigation?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
SOFT navigation (<Link>): fetches only the changed segment's RSC
payload. Shared layouts stay MOUNTED — state, scroll preserved.
HARD navigation (<a>, external URL, full reload): everything
unmounts and reloads from scratch. All client state lost.
Prefetching (on Link viewport entry / hover):
STATIC route → full route prefetched
DYNAMIC route → skipped UNLESS loading.tsx exists
→ then: partial prefetch (shared layout + skeleton)
loading.tsx enables STREAMING: Next.js auto-wraps page.tsx in a
<Suspense> boundary — same mechanism as React's Suspense topic.
Route groups: app/(name)/ — organizes files, can scope a distinct
layout to a subset of routes. The (name) NEVER appears in the URL.
useLinkStatus(): shows a (usually debounced) pending indicator for
slow-network cases where prefetch didn't finish before the click.
ALWAYS use <Link> for internal navigation. Reserve plain <a> for
genuinely external URLs.
The beginner framing: <Link> is how you navigate between routes in a Next.js app — it looks like an <a> tag, but it does considerably more work behind the scenes to keep navigation fast.
The precise mental model: every navigation is fundamentally one of two kinds. A soft navigation (triggered by <Link> or the router) fetches only the RSC payload for the segment that actually changed, keeps every shared layout mounted and interactive, and preserves scroll and client state. A hard navigation (a full browser reload, or a plain <a> tag) discards everything and starts over from scratch — every layout remounts, all client state is lost.
import Link from "next/link";<nav> <Link href="/blog">Blog</Link> {/* prefetched, soft navigation */} <a href="/contact">Contact</a> {/* NOT prefetched, hard navigation */}</nav>
Client-side <Link> navigation vs. a hard browser navigation
step 1 / 4
<Link href="/blog">Blog</Link>
// enters the viewport...
Client<Link> enters the viewport — prefetch starts automatically
Serveridle (nothing requested yet)
Next.js automatically prefetches routes linked with <Link> as they enter the viewport — for a static route, the full route; for a dynamic one, a partial prefetch if loading.tsx exists.
Next.js automatically prefetches <Link> routes as they enter the viewport. How much gets prefetched depends on the route:
Static routes: the full route is prefetched.
Dynamic routes: prefetching is skipped entirely, unless a loading.tsx exists for that segment — in which case the shared layout and the loading skeleton are prefetched (a partial prefetch), so navigation still feels instant even though the real dynamic content isn't ready yet.
Streaming: sending what's ready, without waiting for everything#
loading.tsx (see Loading & Error UI) is what enables streaming for a route segment — Next.js automatically wraps the page in a <Suspense> boundary, showing the prefetched fallback immediately and swapping in the real content once rendering finishes.
Route groups: organizing folders without affecting the URL#
app/ (marketing)/ about/page.tsx → /about (NOT /marketing/about) pricing/page.tsx → /pricing (shop)/ layout.tsx → a DIFFERENT layout, only for shop routes products/page.tsx → /products
Wrapping a folder name in parentheses — (marketing) — creates a route group: it organizes files and can apply a distinct layout to a subset of routes, but the parentheses themselves never appear in the URL.
Predict what happens before checking the solution.
app/ dashboard/ layout.tsx (has NO loading.tsx sibling) page.tsx (fetches data — a dynamic route)
A user hovers over a <Link href="/dashboard"> for a while before clicking. Does the click feel instant?
Solution
No — not fully. Since app/dashboard/ is a dynamic route (it fetches data) and has no loading.tsx, Next.js skips prefetching it entirely — hovering does nothing. When the user finally clicks, the client must wait for the full server response before showing anything, which can feel like the app isn't responding. Adding a loading.tsx to that folder would enable a partial prefetch (the shared layout + a loading skeleton), making the click feel instant even though the real data still needs to load in afterward.
This is the exact decision tree covered above: navigation type first (soft vs. hard), then, for soft navigations, how much gets prefetched ahead of time based on whether the route is static and whether loading.tsx exists.
A soft navigation's ability to keep shared layouts mounted is the exact same "layouts don't re-render on navigation" guarantee from App Router Overview — nothing new is happening mechanically, it's the same nested-component-tree behavior, just triggered by <Link> instead of a raw route change. And streaming a dynamic route's content in after the shell is the identical mechanism from Suspense — loading.tsx is just Next.js automatically wiring up the <Suspense> boundary for you at the route-segment level.
<a href="/dashboard">Dashboard</a> {/* ❌ forces a hard navigation */}
This forfeits prefetching, soft navigation, and layout preservation entirely — always use <Link> for navigation within the app; reserve plain <a> tags for genuinely external URLs.
2. Expecting a dynamic route to feel instant without loading.tsx#
Covered in Try It — without a loading.tsx, a dynamic route isn't prefetched at all, so every click waits for the full server round-trip. This is one of the most common causes of "navigation feels slow" reports.
Always use <Link> for internal navigation — reserve raw <a> tags for external links where a full navigation is actually correct.
Add loading.tsx to any dynamic route where instant-feeling navigation matters — it's the single highest-leverage fix for "clicking a link feels slow."
Use route groups to apply different layouts to different sections of the app without duplicating URL structure or forcing an artificial path segment.
Disable prefetching (prefetch={false}) for very large lists of links (e.g. an infinite-scroll table) to avoid wasted prefetch requests for links the user is unlikely to click.
Prefetching trades a small amount of background network usage for a large perceived-speed win — the default (prefetch on viewport entry) is right for most navigation; only override it for pages with unusually many links.
useLinkStatus lets you show a debounced loading indicator (e.g., after a 100ms delay) specifically for slow-network cases where prefetching didn't finish before the click — a small UX addition that prevents navigation from feeling broken on a bad connection.
Client Components need to hydrate before <Link> can start prefetching — a large initial JS bundle delays this. Keeping the initial bundle lean (see Server/Client Component Composition) has a knock-on benefit for how soon prefetching can even begin.
}
if (hasLoadingFile) {
return { kind: "soft", prefetch: "partial", description: "shared layout + skeleton prefetched, real content streams in after click" };
}
return { kind: "soft", prefetch: "none", description: "no prefetch — client waits for full server response on click" };