В крупномасштабных enterprise-организациях монолитная кодовая база фронтенда неизбежно сталкивается с организационными и технологическими пределами масштабирования. Когда над единым репозиторием работают десятки продуктовых команд, время сборки (next build) возрастает до десятков минут, коллизии слияния веток становятся регулярными, а ошибка в одном второстепенном компоненте способна остановить деплой всей платформы.
Для решения этих ограничений Next.js 16 предлагает архитектурный паттерн Multi-Zones (Многозональность).
В отличие от традиционных микрофронтендов на базе Module Federation или Web Components, Multi-Zones объединяет несколько независимых приложений Next.js на уровне сетевого роутинга. Для конечного пользователя и поисковых систем сайт выглядит как монолитный ресурс с единым доменом (https://enterprise.io), однако разные ветви путей обслуживаются физически независимыми сервисами:
/ и /pricing — маркетинговое приложение (apps/marketing)/dashboard/* — высокоинтерактивный личный кабинет (apps/dashboard)/docs/* — оптимизированная статическая документация (apps/docs)Каждая зона имеет собственный независимый жизненный цикл разработки, стек зависимостей и изолированный пайплайн непрерывной доставки (CI/CD) внутри монорепозитория под управлением Turborepo.
Ментальная модель Multi-Zones базируется на концепции маршрутизирующей хост-зоны (Root Host Zone) и делегированных сервисных зон (Delegated Micro-Apps).
Хост-приложение перехватывает сетевой трафик и на основе префиксов путей выполняет проксирование запросов к нижележащим приложениям. При этом каждая зона компилирует свои статические файлы со специфическим префиксом assetPrefix, исключая коллизии имен JS-чанков (_next/static) в CDN.
Пользователь обращается к: https://company.com/dashboard/billing
│
▼
┌────────────────────────────────────────────────────────┐
│ Root Host Zone (apps/marketing на порту 3000) │
│ 1. Анализирует входящий путь: /dashboard/billing │
│ 2. Сопоставляет правило в next.config.ts rewrites(): │
│ source: '/dashboard/:path*' │
│ destination: 'https://dashboard.internal/:path*' │
└────────────────────────────────────────────────────────┘
│
▼ (HTTP Rewrite по внутренней сети)
┌────────────────────────────────────────────────────────┐
│ Delegated Zone (apps/dashboard на порту 3001) │
│ 1. baseDirectory / basePath: '/dashboard' │
│ 2. assetPrefix: 'https://cdn.company.com/dashboard' │
│ 3. Рендерит RSC-граф страницы billing/page.tsx │
└────────────────────────────────────────────────────────┘
│
▼
Браузер клиента получает единый HTML и JS:
- Домен в строке URL: company.com/dashboard/billing
- Скрипты: cdn.company.com/dashboard/_next/static/...Для совместного использования компонентов, стилей и типов приложения объединяются в монорепозиторий под управлением Turborepo, где переиспользуемые модули выносятся в изолированные внутренние пакеты (packages/ui, packages/auth, packages/tokens).
Топология монорепозитория Turborepo
┌──────────────────────────────────────────────────────────────┐
│ packages/ (Shared Code) │
├──────────────────┬────────────────────┬──────────────────────┤
│ @repo/ui │ @repo/auth-token │ @repo/tailwind-cfg │
│ (Design System) │ (SSO & JWT) │ (Tokens & Styles) │
└────────┬─────────┴─────────┬──────────┴──────────┬───────────┘
│ │ │
├───────────────────┼─────────────────────┤
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ apps/marketing │ │ apps/dashboard │ │ apps/docs │
│ Port 3000 │ │ Port 3001 │ │ Port 3002 │
│ Path: /* │ │ Path: /dashboard│ │ Path: /docs │
└─────────────────┘ └─────────────────┘ └─────────────────┘Входящий HTTP-запрос сопоставляется с таблицей rewrites в next.config.ts хост-приложения или правилами Ingress Controller.
Хост проксирует запрос в целевую зону с сохранением заголовков Cookie, Authorization и x-forwarded-host для прозрачной сквозной аутентификации.
Целевое приложение генерирует HTML, где ссылки на JS-чанки и CSS указывают на уникальный изолированный префикс зоны в объектном хранилище CDN.
При клике на ссылку между зонами (с / на /dashboard) Next.js распознает переход за пределы зоны и выполняет полноценный переход браузера без конфликта Router Cache.
Каждая зона независимо управляет своими Cache Tags и жизненным циклом данных через 'use cache', исключая взаимную блокировку релизов.
Что произойдет, если в архитектуре Multi-Zones для приложений apps/marketing и apps/dashboard НЕ настроить уникальный assetPrefix или basePath?
Публичные маркетинговые страницы, финансовый блог и калькуляторы валют разрабатываются командой привлечения (Growth Team) с частыми A/B-тестами и ISR. В то же время критический личный кабинет с операциями по картам и платежами разрабатывается командой Core Banking с жесткими проверками безопасности. Multi-Zones позволяет релизить маркетинг 10 раз в день, не затрагивая банковский контур.
Каталог товаров и страница оформления заказа (Checkout) разделены на независимые зоны. Команда чекаута использует изолированный кластер серверов с гарантированной производительностью процессора и памяти, предотвращая падение оплаты при пиковых наплывах трафика в каталоге во время Черной Пятницы.
Инженерный блог и портал документации генерируются десятками тысяч статических страниц на Markdown/MDX. Вынесение раздела /docs в отдельную зону изолирует время сборки документации от основного приложения личного кабинета, сокращая CI/CD монорепозитория с 45 минут до 2 минут.
Вместо рискованного «Большого взрыва» (Big Bang Migration), организация постепенно выносит новые разделы (/settings, /billing) в современные Next.js зоны, проксируя остальные страницы на старый монолит через хост-роутер.
apps/marketing/next.config.ts#Корневое приложение слушает основной трафик и настраивает асинхронные rewrites для делегирования путей /dashboard/:path* во вторую зону:
import type { NextConfig } from 'next';
const DASHBOARD_ZONE_URL =
process.env.DASHBOARD_ZONE_URL || 'http://localhost:3001';
const DOCS_ZONE_URL =
process.env.DOCS_ZONE_URL || 'http://localhost:3002';
const nextConfig: NextConfig = {
// Изолируем сборку статики корневого приложения
assetPrefix: process.env.NODE_ENV === 'production' ? 'https://assets.enterprise.io/marketing' : undefined,
async rewrites() {
return {
// afterFiles гарантирует, что локальные страницы маркетинга имеют приоритет
afterFiles: [
// Делегирование запросов личного кабинета
{
source: '/dashboard',
destination: `${DASHBOARD_ZONE_URL}/dashboard`,
},
{
source: '/dashboard/:path*',
destination: `${DASHBOARD_ZONE_URL}/dashboard/:path*`,
},
// Делегирование статических ассетов зоны dashboard
{
source: '/dashboard-static/_next/:path*',
destination: `${DASHBOARD_ZONE_URL}/dashboard-static/_next/:path*`,
},
// Делегирование портала документации
{
source: '/docs',
destination: `${DOCS_ZONE_URL}/docs`,
},
{
source: '/docs/:path*',
destination: `${DOCS_ZONE_URL}/docs/:path*`,
},
],
};
},
};
export default nextConfig;apps/dashboard/next.config.ts#Делегированное приложение конфигурируется с использованием basePath и выделенного assetPrefix:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Базовый путь: приложение ожидает все маршруты внутри /dashboard
basePath: '/dashboard',
// Уникальный префикс статики для исключения коллизий с маркетингом
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://assets.enterprise.io/dashboard'
: '/dashboard-static',
// Поддержка автономного развертывания через Docker
output: 'standalone',
// Изолированные заголовки безопасности
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Zone-Name', value: 'enterprise-dashboard' },
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
];
},
};
export default nextConfig;Запрос пользователя попадает на CDN / Cloudflare / Ingress, который маршрутизирует запрос в хост-зону (apps/marketing).
Альтернативно, маршрутизация по префиксам путей может выполняться непосредственно на уровне Nginx/Envoy Ingress.
Хост-приложение сопоставляет правило /dashboard/:path* и перенаправляет запрос на внутренний микросервис apps/dashboard.
Сетевой вызов происходит во внутренней VPC сети с минимальной задержкой менее 3 мс.
apps/dashboard обрабатывает запрос, генерирует RSC Flight Payload и внедряет ссылки на изолированный assetPrefix.
Зона личного кабинета компилируется полностью изолированно, используя свои версии библиотек.
При переходе между страницами разных зон браузер осуществляет нативный переход документа (Hard Navigation) с сохранением сессионных кук.
Полноценный переход необходим для сброса React Tree и Router Cache между разными версиями бандлов.
Чтобы пользователю не приходилось повторно логиниться при переходе из маркетинга в дашборд, кука сессии выдается на весь корневой домен с флагом Domain=.enterprise.io:
import { cookies } from 'next/headers';
import { jwtVerify, SignJWT } from 'jose';
const SECRET_KEY = new TextEncoder().encode(process.env.AUTH_JWT_SECRET || 'secret-jwt-key');
const COOKIE_NAME = 'enterprise_session';
export interface UserSession {
userId: string;
email: string;
role: 'admin' | 'member' | 'guest';
tenantId: string;
}
/**
* Валидация сессии, вызываемая в серверных компонентах любой зоны
*/
export async function getSession(): Promise<UserSession | null> {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, SECRET_KEY, {
issuer: 'enterprise:sso',
audience: 'enterprise:zones',
});
return {
userId: payload.sub as string,
email: payload.email as string,
role: payload.role as any,
tenantId: payload.tenantId as string,
};
} catch {
return null;
}
}
/**
* Создание сквозной сессионной куки (Domain Cookie)
*/
export async function setSessionCookie(session: UserSession) {
const token = await new SignJWT({ ...session })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(session.userId)
.setIssuer('enterprise:sso')
.setAudience('enterprise:zones')
.setExpirationTime('7d')
.sign(SECRET_KEY);
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
// КРИТИЧЕСКИ ВАЖНО: Точка перед доменом обеспечивает доступность куки для всех зон и поддоменов
domain: process.env.COOKIE_DOMAIN || '.enterprise.io',
maxAge: 7 * 24 * 60 * 60,
});
}CrossZoneLink#Обычный компонент <Link> из Next.js пытается выполнить клиентскую навигацию (Soft Navigation) через fetch RSC-пейлоада. Если ссылка ведет в другую зону, клиентский роутер получит несовместимый HTML и выдаст ошибку. Для межзонных переходов требуется использовать нативный <a> переход:
'use client';
import React from 'react';
import Link from 'next/link';
interface CrossZoneLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
currentZone: 'marketing' | 'dashboard' | 'docs';
children: React.ReactNode;
}
// Определение принадлежности URL к зонам платформы
function getTargetZone(href: string): 'marketing' | 'dashboard' | 'docs' {
if (href.startsWith('/dashboard')) return 'dashboard';
if (href.startsWith('/docs')) return 'docs';
return 'marketing';
}
export function CrossZoneLink({
href,
currentZone,
children,
className,
...props
}: CrossZoneLinkProps) {
const targetZone = getTargetZone(href);
const isCrossZone = targetZone !== currentZone;
// Если ссылка ведет в другую зону, используем нативный HTML <a> для Hard Navigation
if (isCrossZone) {
return (
<a href={href} className={className} {...props}>
{children}
</a>
);
}
// Если ссылка внутри текущей зоны, используем быстрый клиентский переход Next.js
return (
<Link href={href} className={className} {...props}>
{children}
</Link>
);
}turbo.json#{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
"env": [
"NEXT_PUBLIC_*",
"DASHBOARD_ZONE_URL",
"DOCS_ZONE_URL",
"AUTH_JWT_SECRET"
]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
}
}
}Пользователь кликает на ссылку перехода из приложения маркетинга в дашборд. Вместо загрузки дашборда на экране отображается пустая белая страница, а в консоли браузера появляется ошибка: 'SyntaxError: Unexpected token < in JSON at position 0'. Что вызвало сбой?
Разработчики пытаются создать общий глобальный стор на клиенте и ожидать, что состояние корзины или выбранных фильтров сохранится при переходе из зоны /catalog в зону /checkout. Поскольку межзонный переход выполняет навигацию браузера, вся оперативная память JavaScript сбрасывается.
Как избежать: Синхронизируйте межзонное состояние через Cookie, localStorage или распределенную базу данных / серверную сессию.
Если хост-приложение и целевая зона настроены с одинаковым basePath: '/', то при внутреннем rewrite возникает бесконечный цикл перенаправлений (Redirect Loop) либо перехват путей хостом.
Как избежать: Хост-зона всегда обслуживает корень (basePath: ''), а каждая микросервисная зона явно настраивает свой уникальный сегмент пути (basePath: '/dashboard', basePath: '/docs').
Если в зоне /docs использовать относительные пути к статическим ресурсам (/_next/static/...), хост-приложение попытается найти эти файлы в своей локальной директории .next/static и вернет 404 Not Found.
Как избежать: Всегда задавайте уникальный assetPrefix в next.config.ts, указывающий на CDN или выделенный проксируемый маршрут (/docs-static).
Если пакет @repo/ui использует фичи React 19, а одна из зон еще работает на React 18, сборка упадет с ошибкой несоответствия версий рантайма хуков (Invalid Hook Call).
Как избежать: Задавайте peerDependencies в общих пакетах строго через "react": ">=19.0.0" и фиксируйте версии в pnpm-workspace.yaml.