Skip to content

Міграція на Next.js App Router з Pages Router: повний практичний гайд (2026)

App Router - не переопрацьований Pages Router. Це інша парадигма маршрутизації на іншій моделі компонентів, іншому контракті даних і іншій стратегії рендерингу. Сприймати міграцію як реструктуризацію файлів - головне джерело продакшн-інцидентів.

Архітектурна діаграма міграції Next.js - Pages Router vs App Router
Опубліковано:Оновлено:Час читання:16 хв

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.tsxapp/about/page.tsx. Конвенція page.tsx замінює filename-equals-route.
  • Лейаути: pages/_app.tsxapp/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.tsapp/api/hello/route.ts. Route Handlers використовують Web API Request/Response замість NextApiRequest/NextApiResponse.
  • Сторінка 404: pages/404.tsxapp/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/*.tsapp/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.js i18n з Pages Router. Рекомендоване рішення у 2026 - next-intl із Middleware-визначенням локалі та generateMetadata() для hreflang alternates.

Джерела

Схожі статті

React Server Components: як насправді працює архітектура з нульовим бандлом (2026)

Глибоке занурення в React Server Components - модель рендерингу, що прибирає клієнтський JavaScript для серверних піддерев. Wire-формат RSC, семантика меж, async-завантаження даних, Suspense-стримінг, Partial Prerendering та React 19 Compiler - з реальними бенчмарками та практичним фреймворком прийняття рішень.

ReactNext.jsArchitecture
Читати статтю

React 19: useOptimistic, use(), Server Actions - механізм і архітектура (2026)

Детальний розбір п'яти нових примітивів React 19: useOptimistic, use(), Server Actions, useActionState, useFormStatus. Action-based mutation архітектура, що замінює Redux/Zustand для серверних мутацій.

React 19ArchitectureNext.js
Читати статтю

Partial Prerendering (PPR) у продакшні: архітектурні патерни (2026)

Детальний розбір Next.js Partial Prerendering у продакшні. Механізм двофазної відповіді, правила розміщення Suspense-меж, взаємодія з Full Route Cache, дизайн fallback для нульового CLS, виміряні TTFB/LCP, порівняння з ISR+CSR та full SSR і фреймворк прийняття рішень.

Next.jsPerformancePPR
Читати статтю