Parallel routes (@slot) render independent page trees in a single layout — each slot matches its own URL segment
The trap: two slots fetching the same data make duplicate server requests, doubling render time and API costs
Parallel routes are for independently navigable layout sections, NOT for layout composition
Simple layouts with children prop handle 80% of cases — parallel routes add complexity and server load without benefit
default.js must exist at every @slot level or the slot collapses — breaking the layout
Intercepting routes ((.)pattern) create modal-style navigation but only trigger on client-side transitions
✦ Definition~90s read
What is Parallel Routes and Intercepting Routes in Next.js 16?
Parallel routes in Next.js App Router allow a single layout to render multiple independent page trees simultaneously. Defined with @prefix folders (app/@sidebar, app/@main), each slot matches its own URL segments and renders its own components. The parent layout receives each slot as a prop and arranges them visually.
★
Parallel routes are like having two TVs in the same room showing different channels.
Intercepting routes use directory notation ((.)photo) to intercept client-side navigation and render content in a different context — typically a modal overlay. They create dual-behavior URLs where client navigation shows a modal and direct access shows a full page.
The practical distinction: parallel routes handle layout composition with independent navigation (sidebar navigating separately from main content), while intercepting routes handle modal-style navigation for parent-to-child transitions.
Plain-English First
Parallel routes are like having two TVs in the same room showing different channels. Each TV has its own remote (URL). If both TVs show the same show, you pay for two streams. Most layouts only need one TV — the children prop. Parallel routes are for when the sidebar and main content need to navigate independently, like having ESPN on the side TV while the main TV watches HBO.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Parallel routes seem like a clean solution for complex layouts. Two @slots, two independent page trees, one layout. The problem: most teams reach for parallel routes when a simple layout with a children prop would work perfectly — and pay the cost of double server renders, double data fetching, and double API charges.
This article covers when parallel routes actually solve a problem (independent slot navigation), when they silently double your costs (same data in two slots), and how intercepting routes interact with parallel slots to create modal navigation patterns that confuse users.
Tested on Next.js 16.0.0-canary. Parallel route behavior is consistent across Next.js 14+ with App Router.
Parallel Routes Are Not Free — Each @slot Is a Full Page Tree Render
Every @slot in a layout creates an independent page tree. When the user visits a URL, Next.js resolves the main route PLUS every @slot route independently. Each slot matches its own page.tsx, fetches its own data, and renders its own component subtree. The parent layout stitches them together.
The word 'parallel' implies concurrency. It does not exist. Next.js renders slots sequentially: it resolves @slot1/page.tsx, fetches data, renders; then resolves @slot2/page.tsx, fetches data, renders. The total render time is the SUM of all slot render times, not the MAX.
If your layout has 3 slots, each fetching the same 50ms API call, the render time contribution from slots is 3 × 50ms = 150ms — not 50ms. Add the main route's own 100ms fetch, and the total time is 250ms instead of 150ms without slots.
The breakeven: use parallel routes only when slots navigate independently (different URL changes). If all slots show data for the same URL, use a single page with multiple components — not multiple slots.
Slots Render Sequentially — Not in Parallel
Despite the name, parallel routes render one after another within the parent layout. Three slots triple the sequential render time. Do not use parallel routes for layout composition — use them for independent navigation sections.
Production Insight
A monitoring dashboard used 4 slots (@header, @sidebar, @main, @footer). Each slot fetched user data independently. Render time: 480ms for 4 slots + main. After reducing to 2 slots and sharing data: 180ms.
Rule: each slot adds a full Server Component render to the critical path. Profile before adding.
Key Takeaway
Parallel routes render sequentially — total time = sum of all slot times, not max.
Every @slot is a full independent page tree render with its own data fetching.
Keep slot count low (1-2) and share data fetching across slots via the parent layout.
thecodeforge.io
Nextjs Parallel Intercepting Routes
default.js: The Most Common Slot Bug and Why It Breaks Layouts
default.js is the fallback file for parallel route slots. When the current URL has no matching page in a @slot directory, Next.js renders default.js instead. If default.js does not exist, the slot renders null — and that section of the layout collapses.
The collapse is not subtle. If your layout is a CSS grid with 'grid-template-columns: 200px 1fr', and the @sidebar slot collapses, the grid reflows — the @main slot expands to fill the entire width. Content shifts. Layout breaks.
default.js must exist at EVERY level where the slot might be active. If you have app/@modal/page.tsx and app/@modal/feed/page.tsx, you need default.js at BOTH app/@modal/ AND app/@modal/feed/. The slot evaluates page.tsx at the exact URL level — if no match exists, it checks for default.js at the same level, then bubbles up.
app/@modal/default.tsx and app/@modal/feed/default.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ============================================// default.js must exist at EVERY @slot level// ============================================// app/@modal/default.tsx — matches when no modal page at root levelexportdefaultfunctionModalDefault() {
returnnull
}
// app/@modal/feed/default.tsx — matches when in /feed/* with no modalexportdefaultfunctionFeedModalDefault() {
returnnull
}
// app/@modal/feed/(.)photo/[id]/page.tsx — intercepts /photo/:id// from /feed to show in modalexportdefaultasyncfunctionPhotoModal({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await fetch(`https://api.example.com/photos/${id}`)
.then(r => r.json())
return (
<div className="modal-backdrop">
<div className="modal-content">
<img src={photo.url} alt={photo.title} />
<Link href="/feed">Close</Link>
</div>
</div>
)
}
// Without default.tsx at both levels:// /feed → modal slot missing page → collapses (no default.js)// /feed/photo/123 → modal matches intercept → works// Navigating from /feed/photo/123 to /feed → modal collapses entirely
Think of default.js as the Null Object pattern for slots. Every @slot directory (and every subdirectory with a page.tsx in any slot) needs a default.js that returns null or the slot collapses. Default.js is not optional — it is structural.
Production Insight
A social media app's @modal slot worked on /photo/123 but collapsed when navigating back to /feed. The team was missing default.js at the /feed level. The layout jumped every time the modal opened or closed.
Rule: after adding ANY page.tsx inside a @slot directory, add default.tsx at the same level.
Key Takeaway
Missing default.js causes the slot to collapse — breaking the layout grid.
default.js needed at every @slot level where a page.tsx match might be absent.
Always add default.tsx immediately after creating a @slot directory.
Intercepting Routes Only Work Half the Time — Client Navigation vs Direct Access
Intercepting routes use parenthesized-dot notation — (.) for same level, (..) one level up, (..)(..) two levels up, (...) from root. They intercept client-side navigation to show content in a different context (e.g., a photo in a modal instead of a full page).
The catch: intercepting routes ONLY trigger on client-side navigation. Direct URL access, browser refresh, or link opened in a new tab uses the regular route. This means your page renders two different ways depending on how the user arrives.
The typical pattern is a photo gallery where clicking a photo shows a modal (intercepted), but opening the photo's URL directly shows the full photo page. This is intentional — you want both behaviors. But if the intercepting route is meant to replace the regular route entirely (e.g., an admin panel that should always show the modal), you need to handle both paths.
The fix for 'always show modal' is to check navigator.entryType or use a search parameter to force modal mode. If the URL has ?modal=true, render the modal version regardless of navigation source.
Intercepting route with fallback handlingTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// ============================================// Intercepting route with direct-access fallback// ============================================// app/feed/(.)photo/[id]/page.tsx// Only renders when navigated from /feed via client-side LinkexportdefaultasyncfunctionInterceptedPhotoModal({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await fetch(`https://api.example.com/photos/${id}`)
.then(r => r.json())
return (
<div className="modal-backdrop">
<div className="modal-content">
<img src={photo.url} alt={photo.title} />
<p>{photo.description}</p>
<Link href="/feed" className="close-btn">Close</Link>
</div>
</div>
)
}
// app/photo/[id]/page.tsx// Full page render for direct access, refresh, and new tabexportdefaultasyncfunctionPhotoPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await fetch(`https://api.example.com/photos/${id}`)
.then(r => r.json())
return (
<div className="photo-page">
<img src={photo.url} alt={photo.title} />
<h1>{photo.title}</h1>
<p>{photo.description}</p>
<Link href="/feed">Back to gallery</Link>
</div>
)
}
// Option: force modal via searchParams in the regular page// app/photo/[id]/page.tsx with modal flagexportdefaultasyncfunctionPhotoPage({
params,
searchParams
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ modal?: string }>
}) {
const { id } = await params
const { modal } = await searchParams
if (modal === 'true') {
const { default: Modal } = awaitimport('./modal')
return <Modal params={Promise.resolve({ id })} />
}
// ... full page render
}
Intercepting routes behave differently on client navigation vs direct access. Always test: (1) click a Link, (2) type URL directly, (3) refresh the page, (4) open in new tab. All four should produce acceptable results — even if they differ.
Production Insight
A photo app's modal worked perfectly in development but failed in production. The marketing team shared direct URLs to photos, which skipped the intercept and showed the raw page without navigation chrome.
Rule: always design intercepting routes as enhancements, not replacements. The regular route must be complete.
Key Takeaway
Intercepting routes only trigger on client-side navigation — direct access, refresh, new tab skip them.
Always provide a complete regular page as fallback for the intercepted URL.
Use searchParams to force modal behavior when the intercept is non-negotiable.
Parallel Routes vs Shared DataTrade-offs between independent slots and data deduplicationIndependent SlotsShared Data CacheData FetchingEach slot fetches separatelySingle fetch reused across slotsServer LoadDoubled for two slotsMinimized via cachingFlexibilitySlots can differ in data sourcesAll slots share same data shapeImplementationSimple per-slot fetch() callsRequires cache key coordinationPerformanceHigher latency under loadFaster response timesTHECODEFORGE.IO
thecodeforge.io
Nextjs Parallel Intercepting Routes
When Parallel Routes Actually Make Sense: Independent Slot Navigation
Parallel routes shine when each slot navigates independently. The classic pattern: a sidebar that shows different navigation sections while the main content updates for the active section. Each slot maintains its own URL state.
Example: app/@sidebar/users/page.tsx and app/@sidebar/settings/page.tsx. Navigating in the sidebar changes only the @sidebar slot URL. Navigating in the main content changes only the children route. The layout orchestrates both.
Indicators that you genuinely need parallel routes
The sidebar has its own navigation that changes independently of the main content
Multiple sections of the page load async data that should not block each other's initial render
You need modal-style navigation where a 'page within a page' is overlaid
Indicators you do NOT need parallel routes
The sidebar only changes when the main content changes (use a shared layout)
All slots fetch the same data (use a single page with components)
The layout is just header/sidebar/footer with no independent navigation (use a simple layout)
80% of parallel route use cases are better solved with a regular layout and component composition.
app/layout.tsx with independent sidebar navigationTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// app/layout.tsx// Orchestrate sidebar and main content as independent navigable slotsexportdefaultfunctionAppLayout({
children,
sidebar
}: {
children: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div className="flex h-screen">
<nav className="w-64 border-r overflow-y-auto">
{sidebar}
</nav>
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
)
}
// app/@sidebar/page.tsx// Default sidebar — shown when no specific sidebar route matchesimport { SidebarNav } from'@/components/SidebarNav'exportdefaultfunctionSidebarDefault() {
return <SidebarNav />
}
// app/@sidebar/users/page.tsx// Users-specific sidebar — shown when URL is /users/*import { UserNav } from'@/components/UserNav'exportdefaultfunctionUsersSidebar() {
return <UserNav />
}
// app/@sidebar/settings/page.tsx// Settings sidebar — shown when URL is /settings/*import { SettingsNav } from'@/components/SettingsNav'exportdefaultfunctionSettingsSidebar() {
return <SettingsNav />
}
// app/@sidebar/default.tsx// Fallback when sidebar has no matchimport { SidebarNav } from'@/components/SidebarNav'exportdefaultfunctionSidebarFallback() {
return <SidebarNav />
}
If your sidebar does not have its own navigation (different links, different active states), you do not need parallel routes. A layout with children and optional Sidebar component handles it with less complexity. Parallel routes are for — and only for — independently navigating layout sections.
Production Insight
A team used parallel routes for a dashboard that had no independent slot navigation. The sidebar and header always changed together with the main content. They paid double render cost for zero benefit.
Rule: if all slots update together on navigation, use a single layout with components — not parallel routes.
Key Takeaway
Parallel routes are for independently navigating layout sections — not for general layout composition.
80% of layouts should use simple children prop + component composition instead of @slots.
If all slots change together on navigation, you do not need parallel routes.
Intercepting Route Patterns: (.), (..), (..)(..), and (...) Explained
Intercepting routes use relative path notation to specify which level to intercept:
(.) — Intercept at the same level. app/feed/(.)photo/page.tsx intercepts /photo when navigated from /feed.
(..) — Intercept one level up. app/feed/(..)photo/page.tsx intercepts /photo (sibling of /feed).
(...) — Intercept from the root. app/feed/(...)photo/page.tsx intercepts /photo from anywhere when navigated via /feed.
The pattern must match the intended URL exactly. (.)photo at /feed/(.)photo/page.tsx intercepts /feed/photo — not /photo. The level is relative to the intercepting route's position, not the final URL.
Common mistake: using (.) for a root-level intercept. If the intercepting route is at app/feed/(.)product/[id]/page.tsx, it only intercepts /feed/product/123 — not /product/123. For root intercepts, use (...).
(.) Is Relative to the Intercepting File, Not the Final URL
(.)photo in /feed/(.)photo/ intercepts /feed/photo — not /photo. The relative path is from the intercept file's directory to the target. (...) is the only absolute pattern.
Production Insight
A team added (.)product/[id] inside app/(store)/ expecting it to intercept all /product/:id navigation. It only worked within the (store) route group. Root-level /product/:id was never intercepted.
Rule: test intercept scope with the URL bar. If the intercept does not trigger, check the (.) level alignment.
Key Takeaway
Intercepting route levels: (.) same level, (..) one up, (..)(..) two up, (...) root.
The intercept path is relative to the file location, not the final URL path.
(...) is the only pattern that intercepts regardless of the current route depth.
The Performance Profile of Parallel Routes: Slots vs Components
Benchmark comparison: a dashboard page rendered with parallel routes vs the same page with component composition. The test page has a sidebar with user list, main content with product grid, and modal slot. Both versions fetch the same data.
Parallel route version (3 @slots)
Render time: 480ms (150ms sidebar + 180ms main + 150ms modal, sequential within layout)
Data fetches: 14 (sidebar: 4, main: 6, modal: 4 — all independently from each slot)
Data fetches: 7 (deduplicated via React.cache — shared cacheKey for identical fetches)
Bundle size: baseline
The difference: 480ms vs 210ms. 14 fetches vs 7. The parallel route version is substantially worse because each slot lives in its own render context. Even with fetch cache deduplication, React Server Components do not share cache across slot boundaries without explicit React.cache usage.
If you must use parallel routes, use React.cache to share fetches across slots. But the better fix is avoiding parallel routes when slots do not navigate independently.
React.cache to deduplicate across slotsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// ============================================// Shared data fetching across parallel slots// ============================================// lib/data.ts — shared data utilityimport { cache } from'react'exportconst getCurrentUser = cache(async () => {
const res = await fetch('https://api.example.com/auth/me', {
cache: 'force-cache',
headers: {
// Cookie is forwarded from the request — shared across slotsCookie: '' // Set by middleware, accessed via headers()
}
})
if (!res.ok) returnnullreturn res.json()
})
exportconst getDashboardStats = cache(async (userId: string) => {
const res = awaitfetch(
`https://api.example.com/dashboard/${userId}/stats`,
{ next: { revalidate: 60 } }
)
return res.json()
})
// app/@sidebar/page.tsximport { getCurrentUser, getDashboardStats } from'@/lib/data'exportdefaultasyncfunctionSidebarSlot() {
const user = awaitgetCurrentUser()
const stats = await getDashboardStats(user.id) // Uses cached resultreturn <DashboardSidebar user={user} stats={stats} />
}
// app/page.tsx (main content)import { getCurrentUser, getDashboardStats } from'@/lib/data'exportdefaultasyncfunctionDashboardPage() {
const [user, stats] = awaitPromise.all([
getCurrentUser(), // Returns cached — no fetchgetDashboardStats(user.id) // Returns cached — no fetch
])
return <DashboardMain user={user} stats={stats} />
}
Wrap every fetch that might be called from multiple slots in React.cache. It deduplicates within the same request. Without it, each slot makes independent requests for the same data. This is the single highest-impact optimization for parallel route performance.
Production Insight
A team using 3 parallel slots with React.cache reduced API calls from 12 per page to 5. The cache deduplicated 7 identical fetches across slots. Without this optimization, their API provider would have charged $600/month extra.
Rule: React.cache every data fetch that might be shared. The cost is one line per function.
Key Takeaway
Parallel slots do not share fetch cache — each slot makes independent API calls.
Use React.cache to deduplicate shared data fetches across all slots.
Slot-based rendering is 2x slower than component composition for non-independently-navigating layouts.
Diagnosing Slot Rendering Order: Profiling with next build --debug
Next.js build output shows route entries but not parallel slot rendering order. To profile slot rendering, add timing logs to your layout:
Add console.time() calls in the layout that wraps each slot
Run 'next build' and inspect the build output for timing
Alternatively, add a Server Component wrapper that measures React rendering time
The build output format for parallel routes is subtle. Each @slot gets its own route entry in the build output. Run 'npx next build --debug' and look for:
Each @slot line shows its render strategy (○ static, λ dynamic, ƒ ISR). If multiple @slots show λ (dynamic), they all render at request time — no static optimization.
Slot timing profilerTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ============================================// Profile slot rendering time// ============================================// app/layout.tsx with timing instrumentationexportdefaultasyncfunctionProfiledLayout({
children,
sidebar,
modal
}: {
children: React.ReactNode
sidebar: React.ReactNode
modal: React.ReactNode
}) {
// Add timing wrapper in your monitoring tool// Each slot's render time includes its full data fetching treereturn (
<div className="layout">
<section className="sidebar">
{/* @sidebar renders here */}
{sidebar}
</section>
<section className="content">
{/* children renders here */}
{children}
</section>
<section className="modal">
{/* @modal renders here */}
{modal}
</section>
</div>
)
}
// Alternative: check build output for route types// Dynamic (λ) slots indicate request-time rendering for each one// At least one slot being λ means the entire layout is dynamic
If any @slot uses dynamic rendering (λ), the entire layout is dynamic — no static optimization for the whole page. All slots must be static (○) for the layout to be statically optimized. Check 'next build --debug' output.
Production Insight
A team had 2 static slots and 1 dynamic slot. They expected the layout to be partially cached. It was fully dynamic because the dynamic slot forced request-time rendering for all slots.
Rule: one dynamic slot makes the entire layout dynamic. Move dynamic content to client components within a static slot to preserve page-level caching.
Key Takeaway
Every dynamic @slot makes the entire layout dynamic — no static optimization for the page.
Use 'next build --debug' to check each slot's render strategy (○ static, λ dynamic).
Move dynamic content inside a static slot using client components to preserve caching.
● Production incidentPOST-MORTEMseverity: high
Dashboard Layout with 3 Parallel Slots — API Costs Doubled, Render Time Tripled
Symptom
After deploying a new layout with three parallel slots, server render time increased from 150ms to 480ms per request. The authentication API (handling session validation) saw a 3x increase in requests — from 2M to 6M calls per month. CloudWatch bills showed API Gateway costs doubled from $200/month to $400/month. The dashboard itself felt slower to load, with the sidebar rendering before the main content despite both being visible simultaneously.
Assumption
The team assumed parallel routes rendered in parallel (hence the name). They expected total render time to equal the slowest slot, not the sum of all slots.
Root cause
Parallel routes do NOT render in parallel despite the name. Each @slot renders its own page tree sequentially within the parent layout's render cycle. The layout calls renderSlot(@header), then renderSlot(@sidebar), then renderSlot(@main). Each slot independently fetches session data because the layout does not share any fetch cache across slots. Three slots × one session fetch each = three identical API calls per page load. The render time additive because React renders each slot in sequence within the parent Server Component.
Fix
Merged @header and @sidebar into a single @sidebar slot (reducing from 3 to 2 active slots). Moved session fetching to a shared Server Component outside the parallel route structure, passing data as props. Used React.cache to deduplicate fetch calls across all slots. Render time dropped to 180ms. API calls returned to 2M/month.
Key lesson
Parallel routes render SEQUENTIALLY within the parent layout — not in parallel
Each @slot independently fetches its own data — no fetch deduplication across slots
If multiple slots need the same data, lift fetching to the layout and pass via props or use React.cache
Limit parallel slots to 2 max — every additional slot adds full render overhead
Name 'parallel routes' is misleading — they are 'independent routes rendered adjacently'
Production debug guideDiagnosing duplicate fetches and high render times with @slots4 entries
Symptom · 01
API request count doubled after adding parallel routes
→
Fix
Each @slot fetches independently. Check if multiple slots fetch the same data. Move shared data fetching to the parent layout and pass to slots as props. Use React.cache in a shared utility function to deduplicate.
Symptom · 02
Page render time equals sum of all slot render times
→
Fix
Run npx next build --debug and check slot render ordering. Slots render sequentially within the parent. Cannot parallelize — reduce slot count or split into smaller components within a single slot.
Symptom · 03
Slot content disappears on navigation
→
Fix
Missing default.js in the @slot directory. When the current URL has no matching page in the slot, Next.js uses default.js. Without it, the slot renders null — collapsing that section of the layout.
Symptom · 04
Intercepting route not showing modal on page load
→
Fix
Intercepting routes only trigger on client-side navigation. Direct URL access or page refresh uses the normal route. If the modal is missing on first load, conditionally render a full page variant or use searchParams to force modal display.
★ Parallel and Intercepting Routes Quick ReferenceDiagnose slot conflicts and intercept issues
Check for same URL patterns in multiple @slot files
Fix now
Move shared data fetching to the layout file and pass as props to each slot
Layout broken after adding @slot+
Immediate action
Check for missing default.js
Commands
find app -name 'default.tsx' -exec dirname {} \;
ls -d app/@*
Fix now
Add default.tsx to every @slot directory — returning null if the slot should be invisible when unmatched
Modal only shows on client navigation+
Immediate action
Check if intercepting route is being skipped on direct access
Commands
Condition: is the URL accessed by typing vs clicking
Check if intercept route file exists at correct nesting level
Fix now
Add a regular page at the same URL for direct access, or handle with server-side redirect logic
⚙ Quick Reference
5 commands from this guide
File
Command / Code
Purpose
app@modaldefault.tsx and app@modalfeeddefault.tsx
export default function ModalDefault() {
default.js
Intercepting route with fallback handling
export default async function InterceptedPhotoModal({
Intercepting Routes Only Work Half the Time
applayout.tsx with independent sidebar navigation
export default function AppLayout({
When Parallel Routes Actually Make Sense
React.cache to deduplicate across slots
export const getCurrentUser = cache(async () => {
The Performance Profile of Parallel Routes
Slot timing profiler
export default async function ProfiledLayout({
Diagnosing Slot Rendering Order
Key takeaways
1
Parallel routes render sequentially
each @slot adds its full render time to the page
2
Missing default.js in any @slot directory causes that section of the layout to collapse
3
Use React.cache to deduplicate shared data fetching across parallel slots
4
Intercepting routes only trigger on client-side navigation
direct access uses the regular route
5
Only use parallel routes when slots need independent navigation
80% of cases should use component composition
6
One dynamic @slot makes the entire layout dynamic
no static optimization for the page
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How do parallel routes work in Next.js App Router and what are their per...
Q02SENIOR
What is default.js and what happens when it is missing?
Q03SENIOR
A user navigates from /feed to /photo/123 and sees a modal. Another user...
Q04SENIOR
Can you explain the intercepting route notation: (.), (..), (..)(..), an...
Q01 of 04SENIOR
How do parallel routes work in Next.js App Router and what are their performance implications?
ANSWER
Parallel routes use @slot notation to define independent page trees within a single layout. Each @slot resolves its own route from the URL, fetches its own data, and renders its own component tree. The parent layout orchestrates all slots.
The critical performance implication: parallel routes render SEQUENTIALLY within the parent layout, not in parallel. Three slots triple the sequential render time. Additionally, each slot makes independent fetch calls — data is not shared across slot boundaries without explicit React.cache usage.
Practical impact: a layout with 3 slots fetching the same user data makes 3 identical API calls per page load. Render time adds up because React Server Components render each slot one after another within the parent layout's render function.
Use parallel routes only for independently navigating layout sections (sidebar navigates separately from main content). For layout composition (header + sidebar + footer that all change together), use a regular layout with component composition instead.
Q02 of 04SENIOR
What is default.js and what happens when it is missing?
ANSWER
default.js is the fallback component for a parallel route slot when no matching page exists for the current URL. When a @slot directory has no page.tsx that matches the URL path, Next.js checks for default.js. If found, it renders that. If not found, the slot renders null — which collapses that section of the layout and causes layout reflow.
default.js must exist at every @slot level where a page might be absent. If you have app/@modal/ and app/@modal/feed/, you need default.js in both directories. The slot evaluates page.tsx at the current URL level, then falls back to default.js at the same level, then bubbles up to parent levels.
The most common pattern for default.js is returning null for modal slots — the modal is invisible when no modal content exists for the current URL. For sidebar slots, default.js often returns the default navigation component.
Q03 of 04SENIOR
A user navigates from /feed to /photo/123 and sees a modal. Another user opens the same URL directly and sees a full page. Why?
ANSWER
This is the intended behavior of intercepting routes. The first user triggers a client-side navigation (via Link or router.push) from /feed, which activates the intercepting route at /feed/(.)photo/[id]/page.tsx — showing the photo in a modal overlaid on /feed. The second user types /photo/123 directly into the browser, triggering a server-side navigation that skips the intercept and renders the full page at /photo/[id]/page.tsx.
Intercepting routes ONLY trigger on client-side navigation. Direct URL access, browser refresh, and opening in a new tab all use the regular route. This dual behavior is intentional: it allows the UX of a modal for in-app navigation while preserving deep-linking and SEO for direct access.
If you need the modal to always appear (even on direct access), add a search parameter like ?modal=true to the URL and check it in the regular page component to conditionally render the modal layout.
Q04 of 04SENIOR
Can you explain the intercepting route notation: (.), (..), (..)(..), and (...)?
ANSWER
These are relative path patterns that specify which URL level to intercept:
- (.) intercepts at the same level as the current route. app/feed/(.)photo/page.tsx intercepts /feed/photo when navigating from /feed.
- (..) intercepts one level up. app/feed/(..)settings/page.tsx intercepts /settings (sibling of /feed) when navigating from /feed.
- (..)(..) intercepts two levels up. app/feed/category/(..)(..)photo/page.tsx intercepts /photo (two levels above /feed/category).
- (...) intercepts from the root level regardless of depth. app/feed/(...)photo/page.tsx intercepts /photo from anywhere when navigated via /feed.
The path is relative to the intercept file's position, not the final URL. (.)photo inside /feed/(.)photo/ matches /feed/photo — not /photo. For root-level intercepts, always use (...).
A common mistake is using (.) for a pattern meant to intercept globally. Test by clicking a Link vs typing the URL directly — if the intercept does not trigger from Links, the relative level is wrong.
01
How do parallel routes work in Next.js App Router and what are their performance implications?
SENIOR
02
What is default.js and what happens when it is missing?
SENIOR
03
A user navigates from /feed to /photo/123 and sees a modal. Another user opens the same URL directly and sees a full page. Why?
SENIOR
04
Can you explain the intercepting route notation: (.), (..), (..)(..), and (...)?
SENIOR
FAQ · 7 QUESTIONS
Frequently Asked Questions
01
Do parallel routes render in parallel?
No — despite the name, parallel routes render sequentially. The parent layout renders each @slot in order: slot1 renders completely (data fetching + component render), then slot2, then slot3. Total render time is the sum of all slot render times, not the maximum. The name 'parallel' refers to the URL structure (parallel URL spaces), not the execution model.
Was this helpful?
02
What happens if I forget default.js in a parallel route?
The slot collapses entirely — renders null. If your layout uses CSS grid or flexbox, the collapse causes layout reflow. The remaining slots expand to fill the empty space. Content shifts unexpectedly. The fix is to add default.tsx at every @slot directory level, returning null if the slot should be invisible when unmatched.
Was this helpful?
03
Can parallel routes share data without React.cache?
No — each slot fetches data independently. Without React.cache, two slots fetching the same API endpoint make two separate HTTP requests. They are not deduplicated by the Next.js fetch cache because each fetch call lives in a different component context. Wrap shared fetches in React.cache to deduplicate across slot boundaries.
Was this helpful?
04
When should I use intercepting routes vs searchParams for modals?
Use intercepting routes when the modal should only appear during client-side navigation from a specific source (e.g., photo gallery → photo modal). Use searchParams (?modal=true) when the modal should always appear regardless of navigation source. Intercepting + regular route fallback is the recommended pattern — the intercept handles client navigation, the regular route handles direct access.
Was this helpful?
05
Is there a performance cost to having unused @slots?
Yes. Even if a slot renders null (via default.js), Next.js still resolves its route tree during render. The slot's route resolution, data fetching, and Server Component execution happen regardless. An unused slot with default.js returning null still costs ~20-50ms in route resolution overhead. Remove unused slots from the layout completely.
Was this helpful?
06
Can I have nested parallel routes (a parallel route inside a parallel route)?
Technically yes — a @slot can contain another @slot in its layout. This creates deeply nested route trees and compounds the render time problem. Each nested level adds its own slot resolution cost. Avoid nested parallel routes. If you need three levels of slot structure, reconsider the layout architecture entirely.
Was this helpful?
07
How do I test intercepting routes in development?
Intercepting routes only trigger on client-side navigation. In development, use the Link component or router.push() to navigate to the intercepted URL. Direct URL entry in the browser bar or page refresh does NOT trigger intercepting routes. Build a test page with Links to your intercepted routes and click through them during development.