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 - интерактивная поверхность, а не O(n). Для типичных eCommerce-страниц продуктов это снижение гидратационной работы на 65–80% в единицах процессорного времени главного потока.
- Модель компонентов. Pages Router: все компоненты клиентские по умолчанию - имеют доступ к browser API, могут использовать хуки, гидратируются в браузере. App Router: компоненты серверные по умолчанию, выполняются на сервере, имеют прямой доступ к БД и файловой системе, вносят нуль байт в клиентский бандл. Интерактивность - opt-in через директиву
'use client'. - Контракт загрузки данных. Pages Router: загрузка данных вынесена в lifecycle-функции (
getServerSideProps,getStaticProps,getInitialProps) - архитектурно отделена от компонента. App Router: Server Components могут бытьasync-функциями -const data = await db.query(...)валидно внутри Server Component. Lifecycle-функций нет; компонент является единицей загрузки данных. - Модель лейаутов. Pages Router:
_app.tsx/_document.tsx- единый глобальный враппер. Вложенные лейауты - ручные компоненты-обёртки. App Router: вложенные лейауты нативно через файлыlayout.tsxв иерархии директорий. Лейаут вapp/dashboard/layout.tsxоборачивает все маршруты/dashboardи сохраняет React-состояние между навигациями внутри поддерева без ремаунта. - Гранулярность стратегии рендеринга. Pages Router: настройка per-page через экспортируемые функции. App Router: per-fetch через опции
fetch()(cache,next.revalidate,next.tags) и per-route черезexport const dynamicв сегменте - более гранулярно, но требует осознанных архитектурных решений на каждой границе данных.
Часть II - Изменения файловой конвенции
App Router использует отдельную файловую конвенцию, сосуществующую с Pages Router, а не заменяющую его. Это сосуществование - архитектурная основа инкрементальной стратегии миграции.
- Файлы страниц:
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 и показывает UI загрузки. - 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на уровне сегмента маршрута. - API Routes → Route Handlers:
export async function POST(request: Request) { const body = await request.json(); return Response.json({ ok: true }); }. Именованные экспорты по HTTP-методу заменяют method-conditional логику. Web APIRequest/ResponseзаменяетNextApiRequest/NextApiResponse.
Часть IV - Модульный граф webpack и механика границ 'use client'
Директива 'use client' - наиболее значимое решение в App Router-приложении, и наиболее часто неправильно понимаемое. Её поведение определяется алгоритмом построения модульного графа webpack, а не только моделью компонентов React.
При обработке сборки Next.js App Router webpack строит два отдельных модульных графа: серверный граф (RSC-бандл, никогда не отправляемый в браузер) и клиентский граф (браузерный бандл). Директива 'use client' в начале файла - это декларация границы модульного графа: она говорит webpack «этот модуль и все модули, достижимые из него через import, принадлежат клиентскому графу». Критически: граница транзитивна. Если ProductPage.tsx помечен 'use client', то все его импорты (formatPrice.ts, useAnalytics.ts, fetchProduct.ts, ProductImage.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', а<ProductPage>импортирует его вместе с<PriceDisplay>и<ProductImages>(оба Server Components), только<AddToCartButton>и его импорты попадают в браузер. Разница в размере бандла - типично 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/. Продакшн-приложение обслуживает маршруты Pages Router и App Router одновременно без штрафа за само сосуществование.
- Фаза 1 - Корневой лейаут (нулевой риск): Создать
app/layout.tsx, реплицирующий_app.tsx/_document.tsx:<html lang>,<body>, глобальные CSS-импорты, шрифты, аналитика. Директорияpages/не тронута. Верифицировать сборку. Нет видимого пользователю влияния. - Фаза 2 - Статические листовые маршруты: Мигрировать страницы только с
getStaticProps- блог-посты, документация, лендинги. Трансформируются в async Server Components сgenerateStaticParams. Нет клиентской интерактивности для управления. Верифицировать LCP и CLS в CrUX после деплоя. - Фаза 3 - Извлечение утилит данных: Перед миграцией динамических маршрутов извлечь общую загрузку данных в типизированные server-only утилиты (
lib/data.tsсimport 'server-only'). Заменяет паттернgetServerSidePropsboilerplate и делает server-only данные безопасными от случайного клиентского импорта. - Фаза 4 - Динамические страницы с интерактивностью: Наиболее сложная фаза. Паттерн: сегмент маршрута (
app/product/[handle]/page.tsx) - Server Component, получающий данные и рендерящий статическое дерево. Интерактивные компоненты (<AddToCartButton>,<VariantSelector>) -'use client'листья, импортируемые Server Component. Граница'use client'- у листа, а не страницы. - Фаза 5 - API Routes → Route Handlers:
pages/api/*.ts→app/api/*/route.ts. Именованные экспорты по методу. Мигрировать последними - наименьшая связность, наиболее механическое преобразование. - Фаза 6 - Очистка: Удалить директорию
pages/. Убрать остатки_app.tsx,_document.tsx. Запуститьnext buildи верифицировать отсутствие предупреждений об осиротевших маршрутах.
Часть VI - Таксономия ошибок: анализ на уровне механизмов
Постмортемы продакшн-миграций выявляют одни и те же ошибки. Разница между успешными и регрессирующими командами - понимание механизма за каждой ошибкой, а не только симптома.
- Ошибка 1 - Эскалация границы `'use client'`. Пометка каждого компонента
'use client'для исправления ошибок хуков, прогрессивная эскалация до тех пор, пока страница сама не становится Client Component - устраняя все преимущества RSC. Решение:'use client'- исключение, требующее явного обоснования. Только компоненты с хуками, browser API или event handlers требуют его. - Ошибка 2 - Context providers в Server Components. React Context - Client Component API. Оборачивание всего приложения в Context provider принудительно включает каждый дочерний компонент в клиентский бандл. Решение: оборачивать только интерактивное поддерево, требующее контекст, в
'use client'провайдер. - Ошибка 3 - `cookies()`/`headers()` в кэшируемых маршрутах. Чтение
cookies()илиheaders()переводит весь маршрут в динамический рендеринг, отключая Full Route Cache. Решение: перемещать их в наименьшую необходимую область;unstable_noStore()только для конкретного fetch, требующего динамического поведения. - Ошибка 4 - Несериализуемые пропсы через Server/Client-границу. RSC wire format сериализует Server→Client пропсы как JSON. Функции, экземпляры классов и Date не могут пересечь эту границу. Решение: передавать только plain JSON-сериализуемые данные; инкапсулировать серверную логику в Server Actions (
'use server'). - Ошибка 5 - Отсутствие `generateStaticParams` для динамических маршрутов. Без него все сегменты динамических маршрутов дефолтно переходят в динамический рендеринг - каждый запрос попадает на сервер. Решение: реализовать
generateStaticParamsдля всех известных динамических маршрутов для включения CDN-кэширования. - Ошибка 6 - Несовместимые third-party библиотеки. Библиотеки, использующие хуки или
windowна уровне модуля, падают в Server Component окружении. Решение: оборачивать в локальный'use client'файл-обёртку, а не помечать потребляющую страницу как Client Component.
Часть VII - Измеренные результаты производительности
Вот что показывают реальные цифры по продакшн-миграциям с правильным размещением границ 'use client' и реализацией generateStaticParams.
- Размер бандла: Наиболее стабильное улучшение. Типичная eCommerce-страница продукта, ранее доставлявшая 380–520 KB gzip, сокращается до 110–160 KB после RSC-миграции - компоненты отображения данных перемещаются на сервер. Сокращение пропорционально доле дерева, занятой чистым отображением данных против интерактивности.
- LCP: Устранение client-side waterfall (
useEffect → fetch → setState → render) убирает наиболее распространённую причину плохого LCP на динамических страницах. Server Components доставляют предзагруженный HTML в начальном ответе - браузер рендерит немедленно, а не ждёт выполнения JS. - INP: Меньший клиентский бандл означает меньше времени на разбор и компиляцию JavaScript в главном потоке. Сокращение бандла на 300 KB коррелирует с примерно 80–120ms снижением main thread blocking time на mid-tier мобайле, напрямую улучшая INP.
- Time to Interactive: Улучшения TTI коррелируют с сокращением бандла. Меньше JavaScript для разбора означает более быстрый выход браузера в интерактивное состояние - типично на 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 (визарды, real-time collaborative tools) - 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 для валидации сессии для сохранения статического рендеринга. - Какие регрессии бандла отслеживать? Наиболее распространённая регрессия - непреднамеренная эскалация
'use client', фактически превращающая RSC-страницы в CSR. Мониторьте размер бандла через анализаторnext buildи CrUX LCP/INP в процессе миграции. - Поддерживает ли 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
