Попытка единовременного переписывания («Big Bang Rewrite») крупного корпоративного веб-приложения с Pages Router (pages/) на App Router (app/) в 95% случаев заканчивается провалом: остановкой продуктовой разработки на месяцы, регрессионными ошибками в критических бизнес-процессах и потерей выручки. Монолитные кодовые базы содержат сотни тысяч строк кода, унаследованные паттерны getServerSideProps / getInitialProps, глобальные Redux-хранилища и связки сессий.
Инженерным стандартом безопасной модернизации enterprise-систем является паттерн Strangler Fig (Душитель монолита). В Next.js 16 архитектура фреймворка нативно спроектирована для длительного бесконфликтного сосуществования двух маршрутизаторов в рамках одного репозитория: pages/ и app/ работают параллельно, разделяя общий порт, HTTP-сервер и конфигурацию.
┌─────────────────────────────────────────────────────────────────────────────┐
│ STRANGLER FIG COEXISTENCE ARCHITECTURE │
│ │
│ [Incoming HTTP Request] ──► [Next.js Route Resolution Engine] │
│ │ │
│ ┌───────────────────────┴───────────────────────┐ │
│ ▼ ▼ │
│ [App Router Priority Check] [Pages Router Fallback]│
│ app/dashboard/page.tsx pages/checkout.tsx │
│ (React Server Components) (getServerSideProps) │
│ │ │ │
│ ▼ ▼ │
│ [RSC Flight Streaming] [Classic SSR + Hydrate]│
│ │ │ │
│ └───────────────────────┬───────────────────────┘ │
│ ▼ │
│ [Shared Cookie Session / Redis State Synchronization] │
└─────────────────────────────────────────────────────────────────────────────┘Для минимизации ручного труда и исключения человеческого фактора миграция дополняется AST-кодмодами, автоматически переводящими устаревшие хуки useRouter из next/router в next/navigation, заменяющими устаревшие теги и генерирующими серверные границы.
Ментальная модель сосуществования двух роутеров основывается на строгом приоритете разрешения маршрутов (Routing Precedence):
app/analytics/page.tsx и pages/analytics.tsx, Next.js 16 всегда отдаст безусловный приоритет app/. Папка pages/ для данного пути будет полностью проигнорирована.pages/ не могут использовать директиву 'use client' как границу разделения сред: весь код в pages/ собирается по классической модели SPA/SSR с передачей всего JS-бандла в браузер.app/ по умолчанию являются серверными (RSC), изолируя серверную бизнес-логику от бандла клиента.httpOnly Cookie, доступных одновременно как в getServerSideProps(context.req) для Pages, так и в cookies() из next/headers для App Router.ВХОДЯЩИЙ HTTP-ЗАПРОС
│
▼
СОВПАДЕНИЕ В ДИРЕКТОРИИ app/?
│
┌────────────────┴────────────────┐
ДА НЕТ
▼ ▼
[Рендеринг App Router] СОВПАДЕНИЕ В pages/?
- Серверные компоненты │
- Streaming Suspense ┌────────┴────────┐
- Flight Payload ДА НЕТ
▼ ▼
[Рендеринг Pages] [404 Not Found]
- getServerSideProps
- Полная гидратацияУнификация работы с Cookie, отказ от глобального оконного window.state и приведение авторизации к W3C стандартам.
Автоматизированная замена импортов next/router на next/navigation и адаптация ссылок next/link через jscodeshift.
Помещение общих UI-библиотек в независимые пакеты с добавлением директивы 'use client' на интерактивные листья.
Поочередный перенос наименее связанных маршрутов (маркетинг, профиль, аналитика) в каталог app/.
Удаление pages/_app.tsx, pages/_document.tsx и очистка бандла от тяжелых библиотек гидратации.
Команда случайно создала страницу app/catalog/page.tsx, забыв удалить существующий файл pages/catalog.tsx. Как поведет себя Next.js 16 в production-сборке при переходе пользователя по адресу /catalog?
В банковском монолите с 400+ страницами переписать приложение целиком было невозможно из-за непрерывных требований регуляторов по выпуску фичей. Используя Strangler Fig, команда за 6 месяцев перенесла в app/ изолированные разделы: «История операций», «Курсы валют» и «Инвестиции». Высоконагруженный раздел инвестиций получил ускорение First Contentful Paint в 3 раза благодаря RSC и стримингу.
При частичной миграции пользователь постоянно перемещается между страницами старого и нового роутеров. Единый middleware.ts на границе приложения валидирует JWT-токен из Cookie, расшифровывает сессию и прокидывает заголовки x-user-id и x-user-role, делая контекст пользователя идентичным как для getServerSideProps, так и для серверных компонентов app/.
Перевод сотен файлов вручную несет высокий риск опечаток в хуках навигации (router.push vs useRouter из next/navigation). Разработка корпоративного AST-кодмода позволила за 15 секунд переписать 1200 файлов компонентов, корректно заменив методы работы с pathname и query.
В старом приложении все серверные данные загружались в getServerSideProps и гидрировались в гигантский клиентский Redux Store. В App Router данные загружаются прямо внутри изолированных серверных компонентов. Модель Strangler Fig позволила уменьшить размер клиентского бандла Redux на 70%, удаляя ветви стора по мере переноса связанных страниц в app/.
Для того чтобы пользователь не разлогинивался при переходе со страницы pages/account.tsx на app/dashboard/page.tsx, слой работы с сессиями выносится в универсальный адаптер:
import { cookies } from 'next/headers';
import type { IncomingMessage } from 'node:http';
export interface UserSession {
userId: string;
role: 'admin' | 'user';
email: string;
}
/**
* Извлечение сессии в контексте App Router (Next.js 16 async cookies)
*/
export async function getAppRouterSession(): Promise<UserSession | null> {
const cookieStore = await cookies();
const token = cookieStore.get('auth_session')?.value;
if (!token) return null;
return verifyAndDecodeToken(token);
}
/**
* Извлечение сессии в контексте Pages Router (getServerSideProps)
*/
export function getPagesRouterSession(req: IncomingMessage): UserSession | null {
const cookieHeader = req.headers.cookie || '';
const match = cookieHeader.match(/auth_session=([^;]+)/);
const token = match ? match[1] : null;
if (!token) return null;
return verifyAndDecodeToken(token);
}
function verifyAndDecodeToken(token: string): UserSession | null {
try {
// В реальном коде: криптографическая валидация через jose / crypto
const payload = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
return payload as UserSession;
} catch {
return null;
}
}Кодмод находит устаревший импорт useRouter из next/router и заменяет его на раздельные хуки next/navigation с корректной деструктуризацией:
import type { API, FileInfo } from 'jscodeshift';
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(file.source);
// Поиск импорта 'next/router'
const routerImports = root.find(j.ImportDeclaration, {
source: { value: 'next/router' },
});
if (routerImports.length === 0) {
return file.source;
}
// Замена источника импорта на 'next/navigation'
routerImports.forEach((path) => {
path.node.source = j.literal('next/navigation');
});
// Добавление директивы 'use client' в начало файла, если используется useRouter
const hasUseRouter = root
.find(j.CallExpression, {
callee: { name: 'useRouter' },
})
.length > 0;
let output = root.toSource({ quote: 'single' });
if (hasUseRouter && !output.includes("'use client'") && !output.includes('"use client"')) {
output = `'use client';\n\n${output}`;
}
return output;
}Проверка подлинности сессии, дешифрование заголовков безопасности, единый аудит трафика.
Работает прозрачно для обоих роутеров.
Поиск совпадений в дереве app/. При наличии сегмента запрос направляется в конвейер RSC.
Если сегмент не найден, происходит каскадный переход к pages/.
Потоковый рендеринг серверных компонентов, изоляция клиентских бандлов, отправка Flight чанков.
FCP ускоряется на 40-70%.
Вызов getServerSideProps, классическая генерация HTML и полная гидратация клиентского бандла.
Функционирует без изменений до момента плановой миграции.
После переноса компонента из pages/ в app/ разработчик заменил импорт на import { useRouter } from 'next/navigation'. В коде остался вызов const { id } = router.query. В консоли браузера возникла ошибка: TypeError: Cannot read properties of undefined (reading 'query'). В чем причина сбоя?
Попытка вызвать useRouter() из старого пакета next/router внутри компонента каталога app/ приведет к фатальному сбою рантайма: NextRouter was not mounted. Для любых компонентов App Router импорт должен производиться строго из next/navigation.
Директива 'use client' не имеет семантического смысла внутри директории pages/, так как вся эта директория по историческому дизайну компилируется как клиентский код. Добавление этой директивы в pages/ не разделяет серверные и клиентские компоненты и может приводить к ложной уверенности разработчиков в изоляции серверного кода.
Переход по ссылке <Link href="/legacy-page"> со страницы App Router на страницу Pages Router приводит к жесткой перезагрузке страницы (Hard Navigation) с полной переинициализацией DOM-дерева и сбросом клиентского состояния (например, не сохраненных форм или воспроизведения медиа). Для бесшовного перехода группируйте связанные пользовательские сценарии внутри одного роутера.
При механическом переносе логики из компонентов Pages Router в Server Components разработчики часто забывают, что RSC выполняются исключительно на сервере. Обращение к объектам window, localStorage или событиям document.addEventListener внутри RSC завершается падением ReferenceError: window is not defined. Интерактивный код должен быть вынесен в изолированные листья с директивой 'use client'.