Маршрутизація — це архітектурний скелет вебзастосунку, який визначає, як URL зіставляється з виконуваним кодом, даними та деревом компонентів. У традиційних клієнтських клієнтських SPA (на базі react-router) навігація будується навколо централізованого об'єкта конфігурації або декларативних дерев компонентів. При масштабуванні проєкту цей підхід створює критичні системні обмеження:
React.lazy), що за найменшої помилки призводить до витоку зайвих модулів у початковий бандл.Next.js App Router кардинально змінює парадигму завдяки файловій маршрутизації (file-based routing). Структура файлової системи всередині каталогу app напряму компілюється збирачем Turbopack у граф маршрутів застосунку. Це виключає потребу в ручних реєстрах шляхів, гарантує ізоляцію сторінок, реалізує автоматичний code splitting за сегментами та забезпечує нульовий розмір клієнтського бандла (Zero-Bundle Size) для серверних компонентів за замовчуванням.
У класичних React-застосунках маршрутизація налаштовується вручну через код (наприклад, декларативними ланцюжками <Route path="..." element={<Component />} />). У Next.js App Router файлова система і є конфігурацією. Папки формують URL-шляхи, а зарезервовані імена файлів (page.tsx, layout.tsx, error.tsx) декларативно задають структуру React-дерева без жодного рядка роутингового бойлерплейту.
Кожна сторінка компілюється в клієнтський JavaScript-бандл з усіма залежностями.
pages/dashboard.tsx → Client Bundle + getServerSidePropsСерверні компоненти за замовчуванням (RSC). Нуль байтів JS у клієнтському бандлі.
app/dashboard/page.tsx → Zero-Bundle RSC + StreamingУ парадигмі App Router базовою структурною одиницею є сегмент маршруту (Route Segment). Кожна вкладена папка всередині директорії app представляє окремий сегмент URL.
Наприклад, папка app/dashboard/settings точно відповідає URL-шляху /dashboard/settings. Проте саме по собі створення папок не робить їх публічно доступними адресами. Сегмент перетворюється на доступний маршрут лише тоді, коли всередині папки з'являється спеціальний файл — page.tsx. Якщо page.tsx відсутній, сегмент залишається службовою гілкою дерева і використовується виключно для групування маршрутів або успадкування спільних інтерфейсних оболонок.
Кожен сегмент ізольований і гарантує збереження стану (Preserved State): батьківські макети не перемонтовуються заново під час зміни дочірніх сторінок, а дочірні дані передаються у вигляді диференційного потоку React Server Components.
Клікайте по маршрутах та спостерігайте ізоляцію Preserved State у макетах
Перемикайте маршрути: цей текст не скинеться!
Перехід усередині дашборду: DashboardLayout НЕ перемонтовується! Введений пошуковий текст зберігається.
Процес розв'язання маршруту — від кліку користувача по посиланню до фінального узгодження DOM-дерева в браузері — суворо регламентований конвеєром Turbopack та потоковим рушієм React 19:
Браузер ініціює м'яку навігацію (Soft Navigation). Клієнтський роутер надсилає фоновий запит випереджувального завантаження (Prefetch) з HTTP-заголовками RSC: 1 та знімком поточного дерева в Next-Router-State-Tree.
Серверний компілятор зіставляє URL з графом папок app/ і знаходить найменшого спільного предка (LCA) — точку розгалуження між активним і цільовим сегментами.
Сервер виконує виключно змінені серверні компоненти та стрімить дельту у форматі text/x-component (Flight Payload). Спільні макети повторно не виконуються.
Клієнтський рушій React Fiber виконує узгодження піддерева (Subtree Reconciliation), монтуючи нові вузли безпосередньо у слот children найближчого спільного макета. Батьківський DOM і поля введення не скидаються.
Отриманий RSC Flight Payload зберігається в оперативній пам'яті браузера у Router Cache відповідно до правил staleTimes для миттєвої навігації назад/вперед.
Що відбудеться зі станом локального стейту (наприклад, активним пошуковим запитом в input або відкритим дропдауном) всередині app/dashboard/layout.tsx, коли користувач перейде зі сторінки /dashboard/analytics на /dashboard/settings?
Next.js наділяє файли із зарезервованими іменами спеціалізованими системними ролями. Компілятор Turbopack автоматично перетворює ці файли на сувору ієрархію компонентів React.
page.tsx — Єдина точка входу, яка формує публічний URL. Без цього файлу каталог не створює публічний маршрут.layout.tsx — Структурний каркас сегмента. Зобов'язаний приймати проп { children: React.ReactNode }. app/layout.tsx (Root Layout) є обов'язковим для всього проєкту та єдиним місцем оголошення тегів <html> і <body>.loading.tsx — Декларативна межа <Suspense>, яка миттєво віддає скелетон під час серверного завантаження.error.tsx — Ізольований клієнтський перехоплювач збоїв (<ErrorBoundary>). Зобов'язаний містити директиву 'use client'. Якщо дочірня сторінка падає, батьківський макет і навігація залишаються повністю працездатними.not-found.tsx — Сегментний інтерфейс 404, який викликається програмно функцією notFound().(folder) — Route Group (Група маршрутів): дозволяє структурувати проєкт або ізолювати макети без зміни структури URL. Наприклад, app/(marketing)/about/page.tsx доступний за чистим шляхом /about._folder — Private Folder (Приватна папка): виключається із системи маршрутизації.app/
├── layout.tsx # Root Layout (<html> та <body>)
├── page.tsx # Головна сторінка (/)
├── not-found.tsx # Глобальний 404
├── global-error.tsx # Кореневий перехоплювач фатальних помилок
├── (marketing)/ # Route Group (не входить до URL)
│ ├── about/
│ │ └── page.tsx # Маршрут: /about
│ └── pricing/
│ └── page.tsx # Маршрут: /pricing
└── dashboard/
├── layout.tsx # Вкладений каркас із сайдбаром (зберігає стан)
├── loading.tsx # Suspense fallback для дашборду
├── error.tsx # Client Error Boundary ('use client')
├── not-found.tsx # Сегментна 404
├── page.tsx # Маршрут: /dashboard
├── _components/ # Приватна папка (виключена з роутингу)
│ └── MetricCard.tsx
└── settings/
└── page.tsx # Маршрут: /dashboard/settingsПочинаючи з Next.js 15 і стандартів React 19, параметри шляху params та параметри пошуку searchParams передаються в серверні компоненти сторінок, макетів і функцій generateMetadata як асинхронні проміси (Promise).
У синхронній моделі (Next.js 13–14) сервер був змушений затримувати генерацію сторінки доти, доки HTTP-запит не буде повністю розібрано (включаючи всі вхідні заголовки та cookies). Це призводило до штучного блокування часу першого байта (TTFB).
Перехід до Promise — це інфраструктурний фундамент для Partial Prerendering (PPR):
await params обгорнуто в межу React <Suspense>, статична частина сторінки взагалі не чекає на завершення обчислень. Щойно серверний рантайм парсить запит, проміс розв'язується, і сервер досилає динамічні чанки у відкритий потоковий HTTP-канал.import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface AnalyticsPageProps {
params: Promise<{ period: string }>;
searchParams: Promise<{ metric?: string }>;
}
// 1. Динамічна генерація метаданих
export async function generateMetadata(props: AnalyticsPageProps): Promise<Metadata> {
const { period } = await props.params;
return {
title: `Звіт за період: ${period.toUpperCase()}`,
description: `Зведена аналітика за період ${period}`,
};
}
// 2. Серверна сторінка з асинхронними параметрами
export default async function AnalyticsPage(props: AnalyticsPageProps) {
// Асинхронне розв'язання параметрів у Next.js 15+
const params = await props.params;
const searchParams = await props.searchParams;
const validPeriods = ['day', 'week', 'month'];
if (!validPeriods.includes(params.period)) {
notFound();
}
const activeMetric = searchParams.metric ?? 'all';
return (
<main className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Період: {params.period}</h1>
<p className="text-text-muted">Активний фільтр: {activeMetric}</p>
</main>
);
}Якщо нерозв'язаний проміс параметрів передається в клієнтський компонент (Client Component), для його розіменування використовується нативний примітив React 19 — хук use():
'use client';
import { use } from 'react';
interface ClientSectionViewProps {
paramsPromise: Promise<{ section: string }>;
}
export function ClientSectionView({ paramsPromise }: ClientSectionViewProps) {
// Розв'язання промісу параметрів безпосередньо в тілі клієнтського компонента
const { section } = use(paramsPromise);
return (
<div className="p-4 rounded-lg bg-bg-surface border border-border">
<span className="text-xs text-text-muted uppercase">Клієнтський сегмент:</span>
<h3 className="text-lg font-mono font-bold text-accent-cyan">{section}</h3>
</div>
);
}Під час м'якої клієнтської навігації (<Link href="...">) браузер не запитує новий HTML-документ. Замість цього клієнтський роутер надсилає фоновий асинхронний HTTP-запит зі спеціальними системними заголовками:
RSC: 1 — вказує серверу повернути серіалізований потік графа компонентів замість монолітного HTML.Next-Router-State-Tree — передає закодований знімок поточного змонтованого дерева сегментів на клієнті. Сервер обчислює точку найменшого спільного предка (Lowest Common Ancestor, LCA) і виконує виключно змінені компоненти.Аналізуйте сирий бінарно-текстовий протокол серіалізації React Server Components
Запитує Flight stream замість повного HTML
Передає клієнтський знімок для дифу дерева
Прапорець передзавантаження через <Link>
1:I["./components/MetricCard.tsx",["chunks/metric_card_client.js"],"default"]0:{"name":"AnalyticsPage","children":["$","div",null,{"className":"p-6","children":["$","$L1",null,{"metric":"visitors"}]}]}2:L1:{"value":14280,"status":"fresh","timestamp":1741859200}Сервер не відправляє вихідний код MetricCard. Натомість він передає покажчик на бандл-чанк, який браузер має завантажити та гідратувати.
Решта 90% коду сторінки залишається на сервері без роздування клієнтського бандлу (Zero-Bundle Size).
У Next.js 15 політику клієнтського кешування в оперативній пам'яті браузера (Client Router Cache) було перероблено для усунення проблем із застарілими даними:
loading.tsx: Потокові скелетони кешуються на 5 хвилин, запобігаючи мерехтінню спінерів при повторних переходах.Тонке налаштування часу життя кешу на клієнті виконується у next.config.ts:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
staleTimes: {
dynamic: 30, // Кешувати динамічні сторінки в пам'яті браузера на 30 секунд
static: 180, // Кешувати статичні сегменти на 3 хвилини
},
},
};
export default nextConfig;prefetch={null} (за замовчуванням): Для статичних сторінок завантажує повний Flight Payload. Для динамічних сторінок завантажує лише скелетон до найближчого loading.tsx.prefetch={true}: Примусово завантажує повний Flight Payload усього дерева, включаючи динамічні дані.prefetch={false}: Повністю вимикає попереднє завантаження як у в'юпорті, так і при наведенні курсора (hover). Для завантаження за подією розробник викликає router.prefetch() вручну.Навіщо клієнтський роутер під час навігації передає заголовок Next-Router-State-Tree на сервер?
Розміщення компонентів, серверних дій і типів поруч із маршрутом запобігає роздуванню глобальних папок і захищає приватні файли від випадкового перетворення на публічний ендпоінт.
interface Invoice {
id: string;
amount: number;
status: 'paid' | 'pending';
}
interface InvoiceListProps {
items: Invoice[];
}
export function InvoiceList({ items }: InvoiceListProps) {
return (
<ul className="divide-y divide-border border border-border rounded-lg bg-bg-surface">
{items.map((invoice) => (
<li key={invoice.id} className="p-3 flex justify-between items-center text-sm">
<span className="font-mono text-accent-cyan">#{invoice.id}</span>
<span className="font-semibold">${(invoice.amount / 100).toFixed(2)}</span>
</li>
))}
</ul>
);
}import { InvoiceList } from './_components/InvoiceList';
async function getInvoices() {
return [
{ id: 'inv-101', amount: 4900, status: 'paid' as const },
{ id: 'inv-102', amount: 12000, status: 'pending' as const },
];
}
export default async function BillingPage() {
const invoices = await getInvoices();
return (
<section className="space-y-4">
<h2 className="text-xl font-bold">Керування рахунками</h2>
<InvoiceList items={invoices} />
</section>
);
}У файлах page.tsx та layout.tsx можна декларативно задавати стратегію виконання та кешування для всього піддерева сегмента:
// Примусовий динамічний рендеринг для кожного вхідного запиту
export const dynamic = 'force-dynamic';
// Ізоляція середовища виконання (Node.js рантайм)
export const runtime = 'nodejs';
// Час життя кешу сегмента в секундах (Incremental Static Regeneration, ISR)
export const revalidate = 300;
export default async function ReportsPage() {
return <div className="p-6">Зведені звіти (оновлюються кожні 5 хвилин)</div>;
}Під час м'якої навігації (<Link>) клієнтський роутер зберігає в пам'яті попередній активний стан незбіжного слота @slot. Але під час жорсткого перезавантаження сторінки (F5) клієнтський стан очищається. Якщо для слота @analytics фізично відсутня сторінка за цільовим URL і не створено резервний файл default.tsx, Next.js аварійно перериває рендеринг і повертає глобальну помилку 404.
Як уникнути: Завжди оголошуйте резервний файл default.tsx у корені кожного слота паралельного маршруту.
Спроба оголосити app/layout.tsx клієнтським компонентом заради провайдерів тем або контексту авторизації перетворює все піддерево застосунку на Client Components. Це вимикає серверний потоковий рендеринг, блокує прямий доступ до БД із дочірніх сторінок і роздуває клієнтський JavaScript-бандл.
Як уникнути: Виносьте клієнтські провайдери в ізольований дочірній компонент (наприклад, components/providers/ThemeProvider.tsx) та передавайте серверні сторінки через патерн слота {children}.
Розміщення файлу route.ts (API ендпоінт) в одній папці з page.tsx викликає фатальну помилку збірки Turbopack, оскільки один сегмент файлової системи не може одночасно віддавати HTML-інтерфейс і сиру HTTP-відповідь (JSON).
Як уникнути: Розділяйте API-ендпоінти та сторінки за різними каталогами (app/api/users/route.ts та app/users/page.tsx).
У локальному середовищі розробки на Windows (NTFS) або macOS шляхи app/Profile/page.tsx та app/profile/page.tsx відкриваються без помилок через регістронезалежність файлової системи. Проте під час деплою в Linux-контейнер (Vercel, Docker) застосунок падає з помилкою 404 або конфліктом кешу.
Як уникнути: Суворо використовуйте малі літери в kebab-case (app/user-profile/page.tsx) для всіх папок сегментів і підключайте ESLint-правило check-file/folder-naming-convention.