DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/Next.js/Routing & Layouts
beginner25 min read·Updated Jun 2025

Routing & Layouts

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.

routinglayoutsprefetchingstreamingclient-side-navigationroute-groups

Knowledge Check

15 questions · pass at 70%

0/15
easymcq

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.

Related topics

App Router OverviewDynamic RoutesPro

On this page

25m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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.

Prefetching: loading a route before it's clicked#

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.


Try It#

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.


Implement It Yourself#

Model the soft-vs-hard navigation decision and prefetch behavior:

function planNavigation(route, { hasLoadingFile, isStatic }) {
  if (route.startsWith("http") || route.isFullReload) {
    return { kind: "hard", prefetch: "none", description: "full document reload — everything remounts" };
  }
 
  if (isStatic) {
    return { kind: "soft", prefetch: "full", description: "entire route prefetched — instant on click" };













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.


Under the Hood#

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.


Common Mistakes#

1. Using a plain <a> tag for internal navigation#

<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.

3. Assuming route groups affect the URL#

app/(admin)/settings/page.tsx → the URL is STILL /settings, not /admin/settings

Parentheses around a folder name are purely an organizational and layout-scoping tool — they never appear in, or affect, the resulting URL.


Best Practices#

  • 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.

Performance Tips#

  • 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" };
}
planNavigation("/blog", { isStatic: true });
// { kind: "soft", prefetch: "full", ... }
planNavigation("/dashboard", { isStatic: false, hasLoadingFile: false });
// { kind: "soft", prefetch: "none", ... } — feels slow on click