Історично Next.js створювався компанією Vercel із глибокою інтеграцією у власну безсерверну інфраструктуру. При компіляції проєкту (next build) артефакти генеруються всередині директорії .next, яка у монолітному вигляді розрахована або на тривалий запуск процесу node server.js, або на приховану трансляцію у внутрішні примітиви Vercel.
Спроба запустити стандартний бінарник node server.js безпосередньо всередині єдиної функції AWS Lambda призводить до критичних архітектурних проблем:
/_next/static/* пробуджує важкий контейнер Lambda, спалюючи ліміти паралелізму (concurrency) та багаторазово збільшуючи витрати на хмарні обчислення.next/image із нативною бібліотекою sharp навантажують 100% CPU одного інстансу, затримуючи обробку динамічних запитів клієнтів./tmp у Lambda спричиняє втрату даних Incremental Static Regeneration (ISR) та унеможливлює використання тегової інвалідації (revalidateTag) між сотнями паралельних контейнерів.Для вирішення цієї задачі спільнотою було створено OpenNext. OpenNext здійснює зворотний інжиніринг збірки Next.js 15, трансформуючи моноліт у спеціалізовані хмарні сервіси AWS: AWS Lambda (Server, Image, Revalidation), Amazon S3, Amazon CloudFront, Amazon SQS та Amazon DynamoDB.
Топологія розгортання Next.js 15 на базі OpenNext декомпозує та ізолює потоки трафіку на рівні глобальної мережі доставки контенту CloudFront:
Вхідний клієнтський запит (HTTPS)
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Amazon CloudFront CDN (Global Anycast) │
│ - Термінація TLS 1.3, HTTP/3, стиснення Brotli/Gzip │
│ - Маршрутизація за Cache Behaviors на основі шляхів URI │
└───────┬──────────────────────────┬─────────────────────────────┬───────┘
│ /_next/static/* │ /_next/image* │ Default /*
▼ ▼ ▼
┌──────────────────┐ ┌─────────────────────┐ ┌───────────────────┐
│ Amazon S3 │ │ Image Lambda │ │ Server Lambda │
│ (Static Assets) │ │ (sharp, ARM64) │ │ (SSR, Actions) │
│ OAC Restricted │ │ WebP / AVIF Resize │ │ Response Stream │
└──────────────────┘ └──────────┬──────────┘ └─────────┬─────────┘
│ │
▼ │ SWR ISR
┌─────────────────────┐ ▼
│ S3 Assets Storage │ ┌───────────────────┐
│ (Optimized Images) │ │ Amazon SQS │
└─────────────────────┘ │ (ISR Queue) │
│
▼
┌───────────────────┐
│ Revalidation │
│ Lambda Worker │
└─────────┬─────────┘
│
┌─────────────────────────┴─────────┐
▼ ▼
┌─────────────────────┐ ┌───────────────────┐
│ Amazon DynamoDB │ │ Amazon S3 │
│ (Cache Tag Registry)│ │ (ISR HTML/RSC) │
└─────────────────────┘ └───────────────────┘revalidateTag)..next/static) та графіки безпосередньо на зв'язку CloudFront + S3 дозволяє скоротити витрати на вихідний трафік (Egress) та обчислення в 4–7 разів порівняно з комерційними хмарними платформами.При запуску команди npx open-next build утиліта зчитує вихідні файли стандартного компілятора Next.js (.next/standalone, .next/static, .next/server) та формує оптимізований пакет .open-next/:
Генерація автономного серверного входу .next/standalone, клієнтських файлів у .next/static та маніфестів сторінок.
Виділення обробників маршрутів, ізоляція оптимізації зображень та компіляція Lambda-обгорток із потоковим передаванням.
Завантаження клієнтського JavaScript, CSS та медіа-файлів у бакет S3 із заголовками тривалого кешування (immutable).
Зіставлення шляхів запитів із відповідними джерелами: S3 для статики, Lambda Function URL із Response Streaming для SSR.
У Next.js 15 керування зовнішнім кешем здійснюється через інтерфейс cacheHandler у файлі next.config.ts. Нижче наведено приклад адаптера, що зберігає метадані в DynamoDB, а вміст сторінок — в Amazon S3:
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
const s3 = new S3Client({ region: process.env.AWS_REGION });
const ddbRaw = new DynamoDBClient({ region: process.env.AWS_REGION });
const ddb = DynamoDBDocumentClient.from(ddbRaw);
const BUCKET_NAME = process.env.NEXT_CACHE_BUCKET_NAME;
const DDB_TABLE = process.env.NEXT_CACHE_DYNAMO_TABLE;
export default class AwsDistributedCacheHandler {
constructor(options) {
this.options = options;
}
// Отримання запису з кешу за ключем
async get(key) {
try {
// 1. Перевірка метаданих, тегів та часу життя (TTL) у DynamoDB
const meta = await ddb.send(
new GetCommand({
TableName: DDB_TABLE,
Key: { cacheKey: key },
})
);
if (!meta.Item) return null;
// Перевірка завершення абсолютного строку життя запису
if (meta.Item.revalidateAt && Date.now() > meta.Item.revalidateAt) {
return null;
}
// 2. Зчитування вмісту кешу (HTML / RSC JSON) з Amazon S3
const s3Response = await s3.send(
new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: `cache/${key}`,
})
);
const bodyString = await s3Response.Body.transformToString();
const value = JSON.parse(bodyString);
return {
lastModified: meta.Item.lastModified,
value,
};
} catch (error) {
console.error(`[AwsCacheHandler] Cache read error for key: ${key}`, error);
return null;
}
}
// Збереження нових згенерованих даних у розподілене сховище
async set(key, data, ctx) {
try {
const now = Date.now();
const revalidate = typeof ctx.revalidate === 'number' ? ctx.revalidate : false;
const revalidateAt = revalidate ? now + revalidate * 1000 : null;
// 1. Збереження корисного навантаження в S3
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: `cache/${key}`,
Body: JSON.stringify(data),
ContentType: 'application/json',
})
);
// 2. Реєстрація тегів кешу та метаданих у DynamoDB
await ddb.send(
new PutCommand({
TableName: DDB_TABLE,
Item: {
cacheKey: key,
tags: ctx.tags || [],
lastModified: now,
revalidateAt,
},
})
);
} catch (error) {
console.error(`[AwsCacheHandler] Cache write error for key: ${key}`, error);
}
}
// Інвалідація записів за вказаним тегом
async revalidateTag(tag) {
console.log(`[AwsCacheHandler] Invalidating cache tag: ${tag}`);
}
}Нижче наведено еталонний маніфест sst.config.ts на базі рушія SST v3 (Ion). Конфігурація автоматично створює CloudFront, S3-бакет для статики з політикою OAC, таблицю DynamoDB для реєстру тегів ISR, архітектуру ARM64 Graviton3 та права IAM.
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "enterprise-nextjs-aws",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
// 1. Бакет Amazon S3 для надійного збереження кешу ISR
const cacheBucket = new sst.aws.Bucket("NextJsSharedCache", {
access: "private",
});
// 2. Таблиця Amazon DynamoDB для зберігання тегів кешу (revalidateTag)
const cacheTagTable = new sst.aws.Dynamo("NextJsCacheTagRegistry", {
fields: {
cacheKey: "string",
tag: "string",
},
primaryIndex: { hashKey: "cacheKey" },
globalIndexes: {
TagIndex: { hashKey: "tag" },
},
ttl: "expireAt",
});
// 3. Розгортання застосунку Next.js за допомогою рушія OpenNext
const site = new sst.aws.Nextjs("EnterpriseWeb", {
// Використання процесорів ARM64 (AWS Graviton3) забезпечує ~25% виграшу у швидкості та ціні
architecture: "arm64",
memory: "1536 MB",
timeout: "30 seconds",
openNextVersion: "3.1.0",
environment: {
NEXT_CACHE_BUCKET_NAME: cacheBucket.name,
NEXT_CACHE_DYNAMO_TABLE: cacheTagTable.name,
DATABASE_URL: process.env.DATABASE_URL!,
},
// Прив'язка дозволів IAM для взаємодії зі сховищами кешу
link: [cacheBucket, cacheTagTable],
// Налаштування глобальної мережі доставки контенту CloudFront
cdn: {
transform: {
distribution(args) {
// Активація протоколу HTTP/3 для зменшення затримок на мобільних пристроях
args.httpVersion = "http2and3";
// Вимоги до протоколу безпеки TLS
args.viewerCertificate = {
minimumProtocolVersion: "TLSv1.2_2021",
sslSupportMethod: "sni-only",
};
},
},
},
// Ізоляція обробки зображень в окремій функції Lambda
imageOptimization: {
memory: "1536 MB",
staticImageOptimization: true,
},
});
return {
siteUrl: site.url,
distributionId: site.nodes.cdn?.id,
cacheBucketName: cacheBucket.name,
};
},
});Розгортання OpenNext через класичний шлюз AWS API Gateway REST/HTTP API. Браузер очікує поступового завантаження HTML та RSC-фрагментів (loading.tsx), проте екран залишається білим і сторінка відображається повністю лише через 3–5 секунд.
Причина полягає в архітектурі API Gateway: він накопичує всю відповідь функції в оперативній пам'яті й відправляє її лише після повного завершення роботи Lambda. Для коректного стримінгу необхідно використовувати AWS Lambda Function URLs із режимом InvokeMode: RESPONSE_STREAM, який налаштовується в OpenNext автоматично.
// ❌ АНТИПАТЕРН: Робота через звичайний API Gateway без підтримки потокового виводу
// Відповідь буферизується в пам'яті, що ламає Suspense та погіршує показник TTFB
// ✅ ПРАВИЛЬНЕ РІШЕННЯ: Використання Lambda Function URL із Response Streaming
// OpenNext підключає нативну обгортку потокового виводу:
// awslambda.streamifyResponse(async (event, responseStream, context) => { ... })Під час раптового збільшення навантаження до 1000 запитів на секунду AWS Lambda горизонтально створює 1000 паралельних мікроконтейнерів. Якщо кожна безсерверна функція відкриває власний пул підключень до PostgreSQL (max: 10), на базу даних обрушується 10 000 одночасних TCP-з'єднань, що спричиняє падіння СУБД: FATAL: remaining connection slots are reserved for non-replication superuser connections.
// ❌ КАТАСТРОФІЧНА ПОМИЛКА: Пряме некероване підключення до Postgres у Lambda
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
// ✅ ВИРОБНИЧЕ РІШЕННЯ: Маршрутизація через AWS RDS Proxy або PgBouncer
// RDS Proxy утримує стабільний мультиплексований пул підключень до бази даних,
// безпечно розподіляючи транзакції від тисяч короткоживучих інстансів Lambda.Налаштування кешування в CloudFront для кореневого маршруту /* без додавання заголовків Authorization та Cookie до списку Forward Headers (Cache Key). У результаті перший авторизований користувач, який відкрив свій кабінет, кешує приватні дані на CDN, і наступні анонімні відвідувачі бачать чужу інформацію.
// ❌ КРИТИЧНА ВРАЗЛИВІСТЬ: Універсальне кешування динамічних сторінок без ізоляції сесій
// CloudFront Default Behavior: MinTTL = 60, Forward Cookies = None
// ✅ ПРАВИЛЬНЕ РОЗДІЛЕННЯ:
// Статичні файли (/_next/static/*): Кешування на 1 рік, ігнорування кук.
// Динамічні сторінки (/*): CachePolicy = CachingDisabled, OriginRequestPolicy = AllViewerExceptHostHeader.Компіляція Lambda-функцій зі стандартним прапорцем архітектури x86_64. Процесори AWS Graviton3 (arm64) виконують інструкції Node.js V8 приблизно на 20% швидше та коштують на 20% дешевше в перерахунку на мілісекунду роботи. Ігнорування архітектури ARM64 призводить до необґрунтованого збільшення рахунків за хмару.
Перевірка працездатності нативних модулів (наприклад, sharp) під Linux ARM64 та реєстрація зовнішнього адаптера cacheHandler у next.config.ts.
Створення закритого S3-бакета з OAC для статики, таблиці DynamoDB для зберігання тегів та черги SQS для фонової регенерації ISR.
Формування ролей доступу Lambda до S3 та DynamoDB, створення Function URLs із підтримкою Response Streaming для потокового виводу.
Виконання open-next build, перенесення файлів .next/static в S3 із заголовками тривалого кешування та розгортання Server і Image функцій.
Тестування проходження заголовків авторизації, перевірка TTFB під час потокового рендерингу Suspense та перевірка віддачі статики з кодом 200 HIT.