У традиційних багатосторінкових додатках (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 15+ та 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().