Маршрутизация — это архитектурный скелет веб-приложения, определяющий, как 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 16
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% кода страницы остаются на сервере и не попадают в JS-бандл (Zero-Bundle Size).
В Next.js 16 политика кэширования клиентского роутера в оперативной памяти браузера (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). Для загрузки по 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.