В традиционных многостраничных приложениях (MPA) клик по стандартной ссылке <a href="..."> инициирует полный сетевой цикл: браузер запрашивает новый HTML-документ, уничтожает текущий DOM, заново скачивает и исполняет скрипты и стили. Это приводит к потере контекста пользователя (запущенные медиаплееры, состояние несохраненных форм, открытые диалоги) и ухудшению показателей Core Web Vitals (в частности, Interaction to Next Paint, INP).
Next.js App Router реализует гибридную архитектуру маршрутизации. Переходы выполняются как мягкая навигация (Soft Navigation): браузер не перезагружает страницу, а клиентский роутер запрашивает только изменившееся дерево React Server Components (RSC Flight Stream), сохраняя родительские макеты и состояние интерфейса.
В сочетании с интеллектуальным Prefetching (предзагрузкой) и оптимизированным кэшем памяти это позволяет достичь практически мгновенного (0ms perceived latency) переключения между экранами.
Архитектура навигации в Next.js 16 и React 19 базируется на четырех фундаментальных механизмах:
/dashboard/analytics и /dashboard/settings корневой макет и каркас дашборда не перерендериваются. Обновляется только листовой слот children.viewport) через Intersection Observer API.Компонент <Link> входит в область видимости экрана. Intersection Observer инициирует фоновый запрос.
Браузер отправляет HTTP GET с заголовком Next-Router-Prefetch: 1. Сервер возвращает статический каркас или скелетон.
RSC Flight Payload сохраняется в памяти браузера в соответствии с правилами staleTimes (0s для динамических, 300s для статики).
Пользователь кликает по ссылке. Роутер перехватывает клик (preventDefault) и берет готовый UI из памяти без сетевой паузы.
React монтирует новый слот children, переносит фокус ввода на первый доступный элемент сегмента и управляет скроллом.
Чем поведение prefetch={false} в App Router отличается от устаревшего Pages Router?
<Link>#Компонент <Link> из next/link расширяет нативный HTML-тег <a> автоматической предзагрузкой и плавной навигацией:
import Link from 'next/link';
export function NavBar() {
return (
<nav className="flex gap-4 p-4 border-b border-border text-sm">
{/* prefetch={null} (по умолчанию): адаптивный префетч статики и скелетонов */}
<Link href="/dashboard" className="hover:text-accent-violet transition-colors">
Дашборд
</Link>
{/* prefetch={true}: принудительная полная предзагрузка всего дерева */}
<Link href="/analytics" prefetch={true} className="hover:text-accent-violet transition-colors">
Аналитика
</Link>
{/* scroll={false}: переход без прыжка скролла наверх (для табов и пагинации) */}
<Link href="/settings?tab=security" scroll={false} className="hover:text-accent-violet transition-colors">
Настройки безопасности
</Link>
</nav>
);
}В клиентских компонентах программный переход выполняется через хук useRouter(). В React 19 рекомендуется связывать навигацию с хуком useTransition для отображения плавного состояния ожидания (isPending):
'use client';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
export function CategoryFilter() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleSelect = (category: string) => {
// Неблокирующий переход: интерфейс не зависает во время загрузки RSC
startTransition(() => {
router.push(`/catalog?category=${category}`, { scroll: false });
});
};
return (
<div className="flex items-center gap-2">
<button
onClick={() => handleSelect('laptops')}
disabled={isPending}
className="px-3 py-1.5 rounded bg-bg-elevated border border-border text-xs disabled:opacity-50"
>
{isPending ? 'Загрузка...' : 'Ноутбуки'}
</button>
</div>
);
}В серверных компонентах, Route Handlers и Server Actions навигация выполняется специальными серверными функциями, выбрасывающими управляемое прерывание:
import { redirect } from 'next/navigation';
async function getSession() {
return null; // Пользователь не авторизован
}
export default async function DashboardPage() {
const session = await getSession();
if (!session) {
// Вызывает NEXT_REDIRECT и отдает HTTP 307
redirect('/login?from=/dashboard');
}
return <div>Секретная панель</div>;
}Чем вызов router.refresh() отличается от перезагрузки страницы через window.location.reload()?
При переключении вкладок или фильтров пагинации прыжок скролла в координаты (0, 0) раздражает пользователя. Использование опции scroll={false} решает эту проблему:
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
export function DashboardTabs() {
const searchParams = useSearchParams();
const currentTab = searchParams.get('tab') ?? 'overview';
return (
<div className="flex border-b border-border space-x-4">
{['overview', 'logs', 'metrics'].map((tab) => (
<Link
key={tab}
href={`/dashboard?tab=${tab}`}
scroll={false} // Предотвращает скачок страницы наверх
className={`pb-2 text-sm capitalize transition-colors ${
currentTab === tab
? 'border-b-2 border-accent-violet font-semibold text-text-primary'
: 'text-text-muted hover:text-text-primary'
}`}
>
{tab}
</Link>
))}
</div>
);
}Вызов window.location.href = '/dashboard' ломает SPA-архитектуру Next.js, форсируя полную перезагрузку страницы, очистку Client Router Cache и повторную загрузку скриптов.
Как избежать: Всегда используйте компонент <Link> для декларативных ссылок или router.push() из useRouter() для программных действий.
После мутации данных на сервере клиентский роутер может продолжать показывать кэшированные данные из оперативной памяти браузера.
Как избежать: В серверном экшене всегда вызывайте revalidatePath('/dashboard') или revalidateTag('tag'), либо на клиенте вызывайте router.refresh().