Next.js App Router, стабільний з Next.js 13.4 (травень 2023) і повністю зрілий у Next.js 15 (жовтень 2024), - не інкрементальне вдосконалення Pages Router, а паралельна реалізація роутингу на трьох фундаментальних змінах: React Server Components, що переводять модель компонентів від клієнтського дерева до серверного з явними client opt-in межами; конкурентний рендерер React та Suspense streaming, що замінюють синхронне впровадження пропсів на асинхронну доставку HTML-чанків; і файлова конвенція, що піднімає лейаути, стани завантаження та error boundaries з рівня застосунку на рівень фреймворку. У цій статті я розбираю міграцію через чотири виміри: вартісна модель гідратації, яку RSC принципово змінює; механіка модульного графа webpack із правилами розміщення меж 'use client'; внутрішній устрій Data Cache із дедуплікацією та ревалідацією; і інкрементальний протокол міграції з безпечним співіснуванням обох роутерів у продакшні.
Частина I - Вартісна модель гідратації: що не так у Pages Router
Щоб зрозуміти, чому App Router представляє справжній архітектурний прогрес, потрібно спочатку розібратися у вартості гідратації Pages Router і чому ця вартість є структурною, а не випадковою. При рендерингу сторінки в Pages Router через renderToString або renderToPipeableStream створюються два артефакти: (1) HTML-рядок, що доставляється браузеру, та (2) JSON-блоб у __NEXT_DATA__, що містить серіалізоване React virtual DOM дерево всієї сторінки - включаючи всі пропси, дані й стани компонентів. Браузер отримує обидва артефакти та починає гідратацію.
Гідратація в Pages Router - це операція O(n) по всьому дереву компонентів. React обходить кожен компонент - незалежно від наявності інтерактивності - для приєднання event listeners та верифікації збігу client-обчисленого VDOM із серверним HTML. Для сторінки продукту з 50 компонентами, де лише 3 (<AddToCart>, <QuantitySelector>, <WishlistButton>) мають event handlers, React однаково обходить усі 50. JavaScript для всіх 50 компонентів має бути розібраний, скомпільований і виконаний у головному потоці до початку інтерактивності сторінки. Це фундаментальний податок гідратації: вартість пропорційна розміру дерева компонентів, а не інтерактивній поверхні.
App Router змінює це через модель компонентів RSC. Server Components не надсилаються у браузер як JavaScript - вони рендеряться на сервері в HTML і вплітаються у початкову відповідь. JavaScript-рушій браузера ніколи не бачить ці компоненти - немає вартості розбору, компіляції або гідратації. Лише 'use client' компоненти потребують гідратації. Сторінка продукту з 50 компонентами і 3 інтерактивними доставляє JavaScript лише для цих 3 - вартість гідратації O(k), де k - інтерактивна поверхня. Для типових eCommerce-сторінок продуктів це зниження роботи гідратації на 65–80% у одиницях процесорного часу головного потоку.
- Модель компонентів. Pages Router: усі компоненти клієнтські за замовчуванням - мають доступ до browser API, можуть використовувати хуки, гідратуються у браузері. App Router: компоненти серверні за замовчуванням, виконуються на сервері, мають прямий доступ до БД і файлової системи, нуль байт у клієнтський бандл. Інтерактивність - opt-in через директиву
'use client'. - Контракт завантаження даних. Pages Router: завантаження даних у lifecycle-функціях (
getServerSideProps,getStaticProps) - архітектурно відділене від компонента. App Router: Server Components можуть бутиasync-функціями -const data = await db.query(...)є валідним всередині Server Component. Lifecycle-функцій немає; компонент є одиницею завантаження даних. - Модель лейаутів. Pages Router:
_app.tsx/_document.tsx- єдиний глобальний враппер. App Router: вкладені лейаути нативно через файлиlayout.tsxв ієрархії директорій. Лейаут зберігає React-стан між навігаціями без ремаунту. - Гранулярність стратегії рендерингу. Pages Router: per-page через експортовані функції. App Router: per-fetch через опції
fetch()і per-route черезexport const dynamic- більш гранулярно, але потребує свідомих архітектурних рішень на кожній межі даних.
Частина II - Зміни файлової конвенції
- Файли сторінок:
pages/about.tsx→app/about/page.tsx. Конвенціяpage.tsxзамінює filename-equals-route. - Лейаути:
pages/_app.tsx→app/layout.tsx(кореневий лейаут, обов'язковий). Вкладені лейаути:app/dashboard/layout.tsxдля всіх маршрутів/dashboard/*. - Стани завантаження: Новий
app/[route]/loading.tsx- автоматично огортає сторінку у Suspense boundary. - Error boundaries: Новий
app/[route]/error.tsx- автоматично огортає сегмент у React Error Boundary. - API Routes:
pages/api/hello.ts→app/api/hello/route.ts. Route Handlers використовують Web APIRequest/ResponseзамістьNextApiRequest/NextApiResponse. - Сторінка 404:
pages/404.tsx→app/not-found.tsx. Кастомні атрибути<html>/<body>з_document.tsxпереносяться доapp/layout.tsx.
Частина III - Data Cache: дедуплікація запитів та ревалідація зсередини
Один з найменш зрозумілих аспектів міграції - як Data Cache працює на рівні реалізації. У Pages Router завантаження даних винесено у lifecycle-функції саме для запобігання надлишковим API-викликам. В App Router завантаження даних розміщується всередині компонентів - але без надлишкових мережевих запитів. Механізм: Next.js monkey-patching глобальної функції fetch при запуску сервера для перехоплення кожного виклику fetch() під час серверного рендерингу. Поверх цього перехоплення працюють два шари кешування.
- Request Memoization (дедуплікація per-render): В рамках одного серверного проходу рендерингу будь-які два виклики
fetch()з ідентичними URL + method + body повертають одну й ту саму in-memory відповідь - другий виклик ніколи не досягає мережі.<ProductTitle>,<ProductImages>та<ProductSchema>можуть незалежно викликатиfetch('/api/product/123'), і буде зроблено лише один HTTP-запит. Реалізовано черезMap<string, Promise<Response>>з ключем по fingerprint запиту, обмежений деревом рендерингу React. Map очищується між запитами. - Data Cache (cross-render persistence): Відповіді, позначені
next: { revalidate: N }абоnext: { tags: ['...'] }, зберігаються на диску між проходами рендерингу. Сторінка продукту зrevalidate: 3600робить один реальний API-виклик на годину незалежно від обсягу трафіку. Інвалідація за тегами (revalidateTag('product-123')) видаляє конкретний запис кешу; наступний запит регенерує його. Архітектурно це ISR, але що працює з гранулярністю per-fetch, а не per-page. - `getServerSideProps` → async Server Component: Трансформація усуває опосередкованість props-injection. Компонент сторінки -
async; дані завантажуються через звичайнийawaitусередині тіла компонента. Data Cache гарантує відсутність регресії продуктивності. - `getStaticProps` + `getStaticPaths` → `generateStaticParams` + `cache: 'force-cache'`:
generateStaticParams()виконується під час збірки і повертає комбінації параметрів для пре-рендерингу. Режим кешуfetch()за замовчуванням -force-cache, що робить статичний рендеринг opt-out, а не opt-in. ISR налаштовується черезexport const revalidate = Nна рівні сегмента маршруту.
Частина IV - Модульний граф webpack і механіка меж 'use client'
Директива 'use client' - найзначніше рішення в App Router-застосунку і найчастіше неправильно зрозуміле. Її поведінка визначається алгоритмом побудови модульного графа webpack, а не лише моделлю компонентів React. При обробці збірки Next.js App Router webpack будує два окремих модульних графи: серверний граф (RSC-бандл, що ніколи не надсилається у браузер) і клієнтський граф (браузерний бандл). Директива 'use client' на початку файлу - це декларація межі модульного графа: вона каже webpack «цей модуль і всі модулі, досяжні з нього через import, належать клієнтському графу». Межа є транзитивною: якщо ProductPage.tsx позначений 'use client', то всі його імпорти рекурсивно потрапляють до клієнтського бандлу.
- Правильна ментальна модель: Server Components - листя у дереві залежностей модулів, що можуть імпортувати Client Components, але Client Components не можуть імпортувати Server Components. Межа
'use client'позначає корінь клієнтського піддерева. Розміщуйте її якомога глибше у ієрархії компонентів - у найменшого компонента, що реально потребує browser API або event handlers. - Практичний вплив: Компонент
<ProductPage>, що імпортує<PriceDisplay>, який імпортує утилітуformatCurrency.ts- якщо<ProductPage>є'use client', усі три потрапляють у браузер. Якщо лише<AddToCartButton>є'use client', різниця у розмірі бандлу - типово 50–150 KB gzip для реальної сторінки продукту. - Client Reference Objects: Коли Server Component імпортує
'use client'компонент, webpack генерує Client Reference Object - JSON-вказівник на chunk ID у клієнтському маніфесті. Сервер рендерить це посилання у RSC wire format payload; браузер резолвить посилання у реальний компонент зі завантаженого бандлу. Саме тому'use client'компоненти отримують пропси від Server Components безшовно.
Частина V - Протокол інкрементальної міграції
Співіснування обох роутерів в одному Next.js-застосунку - не тимчасовий хак, а повноцінно підтримувана конфігурація. Next.js розв'язує маршрути, перевіряючи app/ перед pages/; маршрут, визначений в обох, використовує реалізацію з app/.
- Фаза 1 - Кореневий лейаут (нульовий ризик): Створити
app/layout.tsx, що реплікує_app.tsx/_document.tsx. Директоріяpages/не зачіпається. Верифікувати збірку. - Фаза 2 - Статичні листові маршрути: Мігрувати сторінки лише з
getStaticProps- блог-пости, документація, лендинги. Перетворюються на async Server Components зgenerateStaticParams. Верифікувати LCP і CLS у CrUX після деплою. - Фаза 3 - Витяг утиліт даних: Витягти спільне завантаження даних у типізовані server-only утиліти (
lib/data.tsзimport 'server-only'). Запобігає випадковому клієнтському імпорту серверних утиліт. - Фаза 4 - Динамічні сторінки з інтерактивністю: Сегмент маршруту - Server Component, що завантажує дані. Інтерактивні компоненти -
'use client'листя. Межа'use client'- у листа, а не сторінки. - Фаза 5 - API Routes → Route Handlers:
pages/api/*.ts→app/api/*/route.ts. Іменовані експорти по методу. Мігрувати останніми. - Фаза 6 - Очищення: Видалити директорію
pages/. Запуститиnext buildта верифікувати відсутність попереджень про осиротілі маршрути.
Частина VI - Таксономія помилок: аналіз на рівні механізмів
- Помилка 1 - Ескалація межі `'use client'`. Позначення кожного компонента
'use client'для виправлення помилок хуків, прогресивна ескалація до того, поки сторінка сама не стає Client Component - усуваючи всі переваги RSC.'use client'- виняток, що потребує явного обґрунтування. - Помилка 2 - Context providers у Server Components. React Context - Client Component API. Огортання цілого застосунку у Context provider примусово включає кожен дочірній компонент у клієнтський бандл. Огортати лише інтерактивне піддерево.
- Помилка 3 - `cookies()`/`headers()` у кешованих маршрутах. Читання
cookies()абоheaders()переводить весь маршрут у динамічний рендеринг, вимикаючи Full Route Cache. Переміщувати у мінімально необхідну область. - Помилка 4 - Несеріалізовані пропси через Server/Client-межу. RSC wire format серіалізує Server→Client пропси як JSON. Функції, екземпляри класів і Date не можуть перетнути цю межу. Передавати лише plain JSON-серіалізовані дані; серверну логіку - у Server Actions.
- Помилка 5 - Відсутність `generateStaticParams` для динамічних маршрутів. Без нього всі сегменти динамічних маршрутів переходять у динамічний рендеринг - кожен запит потрапляє на сервер. CDN-кешування не працює.
- Помилка 6 - Несумісні third-party бібліотеки. Бібліотеки, що використовують хуки або
windowна рівні модуля, падають у Server Component оточенні. Огортати у локальний'use client'файл-обгортку.
Частина VII - Виміряні результати продуктивності
- Розмір бандлу: Типова eCommerce-сторінка продукту, що раніше доставляла 380–520 KB gzip, скорочується до 110–160 KB після RSC-міграції - компоненти відображення даних переміщуються на сервер.
- LCP: Усунення client-side waterfall (
useEffect → fetch → setState → render) прибирає найпоширенішу причину поганого LCP на динамічних сторінках. Server Components доставляють попередньо завантажений HTML у початковій відповіді. - INP: Менший клієнтський бандл означає менше часу на розбір і компіляцію JavaScript у головному потоці. Скорочення бандлу на 300 KB корелює з приблизно 80–120ms зниженням main thread blocking time на mid-tier мобайлі.
- Time to Interactive: Покращення TTI корелюють зі скороченням бандлу - типово на 400–800ms раніше на mid-tier мобайлі при скороченні бандлу на 300+ KB.
Фреймворк прийняття рішень: коли не мігрувати
- Мігруйте, коли: (1) Сторінки переважно data-display з мінімальною інтерактивністю - RSC дає найбільше скорочення бандлу/LCP. (2) Потрібен Partial Prerendering (PPR) - лише App Router. (3) Нові фічі потребують вкладених лейаутів, per-segment UI завантаження або streaming. (4) Server Actions спростять ваші патерни мутацій.
- Відкладіть міграцію, коли: (1) Застосунок - складний stateful SPA - RSC мінімально корисний для клієнтських UI. (2) Команда не знайома з RSC при найближчому дедлайні. (3) Third-party залежності сильно несумісні з Server Components. (4) Поточний застосунок вже проходить всі пороги Core Web Vitals у CrUX - немає тиску продуктивності.
Висновок
App Router - це не просто кращий Pages Router. Він побудований на можливостях, яких просто не було, коли Pages Router проєктувався: нульові серверні компоненти, нативні вкладені лейаути, Route Handlers на базі Web API. Все це тепер частина фреймворку з коробки.
Інкрементальний шлях безпечний - обидва роутери співіснують у Next.js 15, стратегія маршрут-за-маршрутом усуває ризик big-bang переписування. Усі описані вище помилки уникаємі, якщо розуміти механізм. Для data-heavy застосунків виграш у продуктивності реальний і видно у CrUX field data вже за кілька тижнів після деплою.
Для архітектурного ревʼю, скоупінгу міграції або реалізації App Router - сервіс архітектури | кейси | обговорити завдання.
FAQ
- Чи можуть Pages Router і App Router співіснувати в одному Next.js-застосунку? Так. Next.js 13+ підтримує одночасно
pages/іapp/. Маршрути вapp/мають пріоритет. Рекомендується інкрементальний підхід маршрут-за-маршрутом. - Чи підтримує App Router `getInitialProps`? Ні. Якщо використовується у
_app.tsxдля глобальних даних, необхідно відрефакторити до Server Component data fetching перед міграцією кореневого лейауту. - Як керувати автентифікацією в App Router? Дані сесії доступні через
cookies()у Server Components або валідуються у Middleware. Auth.js v5 (next-auth) має першокласну підтримку App Router. Читання сесії в Server Component переводить маршрут у динамічний рендеринг - використовуйте Middleware для валідації сесії для збереження статичного рендерингу. - Чи підтримує App Router i18n? В App Router немає вбудованого i18n-роутингу, еквівалентного
next.config.jsi18n з Pages Router. Рекомендоване рішення у 2026 -next-intlіз Middleware-визначенням локалі таgenerateMetadata()для hreflang alternates.
Джерела
- Next.js Team. 'App Router Migration Guide': https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration
- Next.js Team. 'Next.js 15 Release Notes': https://nextjs.org/blog/next-15
- React Team. 'React Server Components': https://react.dev/reference/rsc/server-components
- Vercel. 'Partial Prerendering': https://vercel.com/blog/partial-prerendering
- Next.js Team. 'Data Fetching, Caching, and Revalidating': https://nextjs.org/docs/app/building-your-application/data-fetching
- Next.js Team. 'Route Handlers': https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- Next.js Team. 'generateStaticParams': https://nextjs.org/docs/app/api-reference/functions/generate-static-params
- Auth.js. 'Next.js App Router Integration': https://authjs.dev/getting-started/installation
- amannn. 'next-intl App Router Guide': https://next-intl-docs.vercel.app/docs/getting-started/app-router
- HTTP Archive. 'Web Almanac 2025 - JavaScript': https://almanac.httparchive.org
