Исторически развертывание Next.js вне платформы Vercel представляло собой серьезный инженерный вызов: разработчикам приходилось либо запускать тяжелый монолитный процесс next start внутри Docker-контейнера, либо вручную деконструировать закрытые внутренности каталога .next (с помощью неофициальных утилит OpenNext или SST). Такие решения были подвержены поломкам при каждом минорном обновлении фреймворка из-за отсутствия стабильного программного интерфейса для компиляции и упаковки серверных функций.
В Next.js 16 архитектура деплоя стандартизирована через Adapters API (с параметром adapterPath в next.config.ts). Компилятор Next.js больше не привязан к конкретному хостингу. На выходе фазы сборки он генерирует универсальную спецификацию сборки: маршруты, манифесты промежуточных слоев (Middleware), статические ассеты, точки входа серверных функций Node.js, легковесные обработчики Edge и потоки частичного предрендеринга (PPR).
┌─────────────────────────────────────────────────────────────────────────────┐
│ NEXT.JS 16 UNIFIED ADAPTER PIPELINE │
│ │
│ [Source Code] ──► [next build --turbo] ──► [Universal Build Output] │
│ │ │
│ ▼ │
│ [Adapters API (adapterPath)] │
│ │ │
│ ┌────────────────────────┬───────────────────┴────────────────┐ │
│ ▼ ▼ ▼ │
│ [AWS Lambda / S3] [Cloudflare Workers] [Kubernetes] │
│ - CloudFront CDN - Edge Runtime / V8 Isolates - Standalone │
│ - SQS Cache Queue - KV Cache / R2 Storage - Pod Cluster │
└─────────────────────────────────────────────────────────────────────────────┘Кастомный адаптер берет на себя трансформацию универсального артефакта в целевые манифесты конкретного облака: разбивает серверные функции на AWS Lambda или Cloudflare Workers, транслирует перенаправления в правила CDN и подключает распределенный кэш (Redis, DynamoDB, Cloudflare KV).
Ментальная модель Adapters API базируется на концепции «Ядро компиляции — Спецификация — Платформенный рантайм»:
routes-manifest.json, генерирует скомпилированные функции и определяет рантайм для каждого сегмента (nodejs или edge).adapterPath): Сборщик передает управление кастомному модулю адаптера, передавая ему метаданные билда: список статических файлов, сопоставление маршрутов (Route Table) и скомпилированные бандлы./static в S3._routes.json.output: 'standalone' с минимизацией слоев до менее 120 МБ.[Next.js Core Compilation]
│
▼
[Universal Build Specification]
- routes-manifest.json
- prerender-manifest.json
- server/chunks, server/pages, static
│
▼
[Custom Adapter Hook]
(export default function buildAdapter(api) { ... })
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
[Target: AWS OpenNext] [Target: Cloudflare] [Target: K8s Enterprise]
- Server Function ZIP - _worker.js bundle - minimal dist/server.js
- Image Optimization - Workerd bindings - In-Memory LRU Cache
- DynamoDB Tag Cache - KV/R2 assets - Pod Health ProbesTurbopack компилирует клиентские бандлы, RSC Flight структуры и генерирует метаданные сборки.
Next.js передает контекст сборки (Build Output API) в функцию кастомного адаптера.
Адаптер анализирует манифест частичного предрендеринга для разделения статической оболочки и динамических стримов.
Генерация энтрипоинтов под интерфейсы AWS Lambda (aws-lambda handler) или Cloudflare FetchEvent.
Формирование готовых к развертыванию пакетов (Terraform, Docker Image, Wrangler bundle).
Зачем в Next.js 16 был введен официальный Adapters API вместо использования встроенного режима standalone или сторонних скриптов сборки?
Многие корпорации юридически не имеют права размещать данные в сторонних облаках (SaaS-платформах). Кастомный адаптер упаковывает Next.js в нативный стек AWS: маршрутизация CloudFront, вычисления на AWS Lambda (Node.js 22 LTS), статика в зашифрованных S3 бакетах и тегированный кэш (revalidateTag) в таблице DynamoDB.
Для международных e-commerce систем критически важно время отклика по всему миру. Адаптер для Cloudflare компилирует серверные компоненты для рантайма workerd (V8 Isolates). Статические ассеты отдаются из локальных дата-центров Cloudflare R2, а динамические PPR-стримы запускаются в пределах менее 10 мс от конечного пользователя.
При развертывании в собственной инфраструктуре на серверах компании стандартный Docker-образ node:alpine может занимать свыше 1 ГБ из-за node_modules. Адаптер на основе output: 'standalone' собирает ультра-легковесный образ с бинарным сервером на Rust/Go или оптимизированным Node.js энтрипоинтом весом менее 80 МБ, что ускоряет масштабирование подов (Horizontal Pod Autoscaler) с 40 секунд до 2 секунд.
Высоконадежные enterprise-системы уровня Tier-4 настраивают одновременную сборку приложения под несколько облаков. При падении региона AWS Route 53 перенаправляет DNS-трафик на резервный кластер в Google Cloud Kubernetes, где развернут тот же самый универсальный артефакт сборки Next.js.
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Активация частичного предрендеринга (PPR)
experimental: {
ppr: 'incremental',
// Путь к кастомному модулю адаптера
adapterPath: require.resolve('./adapters/aws-enterprise-adapter.ts'),
},
// Генерация чистых автономных зависимостей
output: 'standalone',
};
export default nextConfig;Кастомный адаптер экспортирует функцию по умолчанию, получающую интерфейс доступа к артефактам сборки Next.js:
import fs from 'node:fs';
import path from 'node:path';
export interface NextBuildAdapterContext {
routes: Array<{
page: string;
regex: string;
isDynamic: boolean;
runtime: 'nodejs' | 'edge';
}>;
prerenderManifest: {
version: number;
routes: Record<string, { initialRevalidateSeconds: number | false }>;
};
outputDirectory: string; // путь к .next
distDirectory: string; // путь назначения артефактов
}
/**
* Кастомный адаптер Next.js 16 для целевого облака AWS Lambda + S3
*/
export default async function buildAdapter(context: NextBuildAdapterContext) {
console.info('[Adapters API] Запуск генерации артефактов под целевое облако...');
const deployTargetDir = path.resolve(process.cwd(), '.deploy-artifacts');
fs.mkdirSync(deployTargetDir, { recursive: true });
// 1. Извлечение статических файлов для загрузки в S3 / CloudFront
const staticSourceDir = path.join(context.outputDirectory, 'static');
const staticTargetDir = path.join(deployTargetDir, 's3-assets', '_next', 'static');
fs.cpSync(staticSourceDir, staticTargetDir, { recursive: true });
// 2. Генерация серверной точки входа AWS Lambda Handler
const lambdaHandlerCode = `
const { createServer } = require('http');
const path = require('path');
const serverless = require('@codegenie/serverless-express');
// Подключение standalone сервера Next.js
const NextServer = require('./server.js');
// Экспорт стандартного обработчика для AWS Lambda
exports.handler = serverless({
app: NextServer,
binaryMimeTypes: ['image/*', 'font/*'],
});
`;
fs.writeFileSync(
path.join(deployTargetDir, 'lambda-entry.js'),
lambdaHandlerCode,
'utf-8'
);
// 3. Формирование таблицы маршрутизации CloudFront Routing Map
const routingTable = context.routes.map((route) => ({
pattern: route.regex,
target: route.runtime === 'edge' ? 'CloudFront-Edge' : 'AWS-Lambda-Core',
isPrerendered: Boolean(context.prerenderManifest.routes[route.page]),
}));
fs.writeFileSync(
path.join(deployTargetDir, 'cloudfront-routes.json'),
JSON.stringify(routingTable, null, 2),
'utf-8'
);
console.info(`[Adapters API] Успешно сформировано ${routingTable.length} маршрутов в ${deployTargetDir}`);
}При частичном предрендеринге (PPR) сервер должен немедленно вернуть статическую оболочку, а затем удерживать HTTP-поток открытым для передачи динамических чанков React Flight:
import { Readable } from 'node:stream';
/**
* Мост между Web Streams API (Next.js PPR) и Node.js ServerResponse
*/
export async function pipeWebStreamToNodeResponse(
webStream: ReadableStream<Uint8Array>,
nodeRes: import('node:http').ServerResponse
) {
// Установка критических заголовков для предотвращения буферизации на прокси
nodeRes.setHeader('Content-Type', 'text/html; charset=utf-8');
nodeRes.setHeader('Transfer-Encoding', 'chunked');
nodeRes.setHeader('X-Accel-Buffering', 'no'); // Отключение буферизации Nginx
nodeRes.setHeader('Cache-Control', 'no-cache, no-transform');
// Преобразование Web ReadableStream в нативный Node.js Readable
const nodeReadable = Readable.fromWeb(webStream as any);
return new Promise<void>((resolve, reject) => {
nodeReadable.pipe(nodeRes);
nodeReadable.on('end', () => resolve());
nodeReadable.on('error', (err) => {
console.error('[StreamBridge Error]', err);
nodeRes.destroy(err);
reject(err);
});
});
}Turbopack собирает страницы, Server Actions, генерирует маршруты и компилирует автономный сервер.
Создает универсальный манифест .next/routes-manifest.json.
Next.js загружает пользовательский модуль адаптера и передает объект контекста со всеми манифестами.
Позволяет переопределить стратегию распределения вычислительных слоев.
Анализ полей runtime: 'nodejs' | 'edge' и маршрутов частичного предрендеринга PPR.
Формирует независимые пакеты под AWS Lambda и V8 Isolates.
Создание ZIP-архивов, манифестов CloudFormation, Dockerfile или бандлов Wrangler.
Артефакты готовы к доставке через пайплайн CI/CD.
При развертывании Next.js с поддержкой PPR в собственный кластер Kubernetes через кастомный адаптер страницы открываются с задержкой в 3 секунды, а затем мгновенно отображают весь контент без потокового появления Suspense-скелетонов. В чем причина деградации потокового рендера?
Некоторые разработчики по старой привычке пишут shell-скрипты, перемещающие файлы из .next/server/app. Любое внутреннее изменение структуры каталогов в Next.js 16 ломает такую сборку. Всегда читайте данные исключительно через официальный контракт NextBuildAdapterContext и стандартизированные манифесты (routes-manifest.json, prerender-manifest.json).
Если маршрут помечен директивой export const runtime = 'edge', упаковка его в стандартный Node.js контейнер с модулями node:fs или node:net приведет к несовместимости с платформой Cloudflare Workers (workerd). Кастомный адаптер обязан анализировать свойство route.runtime и направлять код в соответствующий специализированный бандл.
При выгрузке директории _next/static в S3 бакет с длительными заголовками Cache-Control: public, max-age=31536000, immutable статические чанки кэшируются вечно. Однако манифесты маршрутов и HTML-страницы не должны кэшироваться неизменяемо. Ошибочная установка заголовка immutable на корневой HTML приводит к тому, что пользователи продолжают загружать старую версию приложения после релиза.
В бессерверных средах (AWS Lambda) после завершения HTTP-ответа микро-виртуальная машина V8 замораживается (freeze). Если фоновые задачи (логирование, запись телеметрии) не были завершены до закрытия потока ответа, они зависают и продолжают выполняться только при следующем «теплом» вызове другого пользователя, что приводит к утечке контекста между запросами.