В классической серверной архитектуре разработчики регулярно сталкиваются с задачей выполнения второстепенных побочных эффектов: записи аудит-лога в базу данных, отправки метрик телеметрии (OpenTelemetry/Datadog), отправки webhook-нотификаций или упреждающего прогрева кэша.
В традиционном Node.js для этого нередко использовали «плавающие» промисы (floating promises):
// ❌ АНТИПАТТЕРН: Неконтролируемый фоновый промис
doAsyncAuditLogging().catch(console.error);
return NextResponse.json({ ok: true });В современных серверных средах (Serverless, AWS Lambda, Cloudflare Workers, Vercel Functions, контейнеры с жестким автомасштабированием) этот подход приводит к фатальным сбоям:
res.end()). Контейнер немедленно приостанавливается, а фоновый промис обрывается на середине сетевого сокета к базе данных.UnhandledPromiseRejection и аварийное завершение процесса Node.js.await doAsyncAuditLogging(), пользователь расплачивается задержкой отклика страницы (+100–300 мс к TTFB), ожидая завершения некритичной для него служебной аналитики.В Next.js 16 представлены две специализированные низкоуровневые функции из модуля next/server:
after() — планирует гарантированное неблокирующее выполнение фоновых задач строго после отправки заголовков и завершения стрима ответа клиенту.connection() — позволяет серверному компоненту явно задекларировать необходимость динамического I/O в парадигме Dynamic I/O.after()#Функция after() связывает переданный асинхронный колбэк с контекстом жизненного цикла текущего HTTP-запроса:
ХРОНОЛОГИЯ HTTP-СОЕДИНЕНИЯ И ПОТОКА AFTER():
Браузер Сервер Next.js (Node / Serverless) БД / Телеметрия / Log
│ │ │
(1)├─── POST /actions/checkout ───────►│ │
│ ├── Основная бизнес-логика │
│ │ (списание баланса, заказ) ────►│
│ │ │
│ ├── Регистрация: after(doAudit) │
│ │ │
(2)│◄── HTTP 200 OK (Стрим закрыт!) ───┤ │
│ [Пользователь видит успех! │ │
│ Задержка TTFB минимизирована] │ │
│ │ │
(3)│ ├── ВЫПОЛНЕНИЕ AFTER-ЗАДАЧ: │
│ │ ├── Отправка OpenTelemetry ───►│
│ │ ├── Аудит транзакции ─────────►│
│ │ └── Прогрев кэша каталога ────►│
│ │ │
│ └── Все задачи after() завершены. │
│ Контейнер безопасно усыпляется.│Главное отличие after() от «плавающего» промиса: платформа знает о существовании зарегистрированных задач и удерживает жизненный цикл серверного процесса/лямбды активным до тех пор, пока все зарегистрированные промисы after() не завершатся, не задерживая при этом пользователя.
connection()#В Next.js 16 с включенным Dynamic I/O компилятор пытается статически пререндерить максимальное количество компонентов во время сборки (next build). Если серверный компонент не считывает cookies() или headers(), компилятор посчитает его статическим.
Что делать, если компоненту требуется свежее сетевое окружение при каждом входящем TCP-соединении (например, генерация случайных рекомендаций, чтение времени хоста, выборка живых биржевых курсов без заголовков)?
Вызов await connection() служит компиляторным маркером динамики:
await connection() генерирует сигнал React.postpone(), останавливая статический пререндер данной ветки.<Suspense>.Серверный компонент вызывает await connection(). Компилятор Dynamic I/O изолирует его в динамический слот внутри Suspense.
Сервер рендерит интерфейс или выполняет мутацию в Server Action. Регистрируются фоновые задачи через after().
HTTP-ответ (HTML/Flight Payload) полностью передается клиенту. Соединение с браузером закрывается. Пользователь видит интерфейс.
Рантайм удерживает процесс активным и исполняет все задачи after() (логирование, сбор метрик, отправка вебхуков).
Почему в Serverless окружении (AWS Lambda / Vercel Functions) вызов after(async () => { await sendMetrics(); }) надежнее обычного вызова sendMetrics() без await перед return?
При выполнении операции перевода средств в Server Action критически важно записать запись в таблицу audit_logs с IP-адресом, цифровой подписью и хэшем транзакции. Использование after() позволяет не увеличивать время отклика для пользователя: перевод подтверждается мгновенно, а запись аудита надёжно фиксируется в фоне.
Формирование спанов распределенной трассировки и отправка пакетов метрик по протоколу OTLP/HTTP. Перенос отправки батча в after() разгружает критический путь рендеринга страницы.
Когда пользователь заходит на страницу категории товаров №1, с вероятностью 85% следующим шагом он откроет карточку топового товара этой категории. Внутри after() вызывается фоновый вызов кэшируемой функции getCachedProduct(topProductId) с директивой 'use cache', наполняя L2 Redis кэш до того, как пользователь нажмет на ссылку.
connection()#Виджет курсов криптовалют или случайных промокодов не зависит от куки пользователя, но обязан выдавать уникальные значения при каждом посещении страницы. await connection() гарантирует динамическое исполнение на сервере без превращения всей страницы в медленный SSR.
after()#'use server';
import { after } from 'next/server';
import { db } from '@/lib/db';
import { sendTelemetrySpan } from '@/lib/telemetry';
import { notifyTelegramAlert } from '@/lib/alerts';
export async function processTransferAction(fromAccountId: string, toAccountId: string, amount: number) {
const startTime = performance.now();
// 1. Критическая бизнес-транзакция (блокирует ответ клиенту)
const transaction = await db.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: fromAccountId },
data: { balance: { decrement: amount } },
});
if (from.balance < 0) {
throw new Error('Недостаточно средств на счете');
}
return tx.account.update({
where: { id: toAccountId },
data: { balance: { increment: amount } },
});
});
// 2. Регистрация неблокирующих фоновых задач
after(async () => {
const duration = performance.now() - startTime;
try {
// Запись в аудит-лог (не задерживает пользователя!)
await db.auditLog.create({
data: {
event: 'TRANSFER_COMPLETED',
fromAccountId,
toAccountId,
amount,
durationMs: Math.round(duration),
},
});
// Отправка метрики в OpenTelemetry
await sendTelemetrySpan('processTransferAction', { duration, amount });
// Если сумма перевода крупная — отправляем уведомление в службу безопасности
if (amount >= 500_000) {
await notifyTelegramAlert(`Крупный перевод: $${amount} со счета ${fromAccountId}`);
}
} catch (backgroundError: unknown) {
// Ошибки в after обязаны перехватываться локально
console.error('[Background Task Error]:', backgroundError);
}
});
// Ответ клиенту возвращается немедленно после завершения транзакции
return { success: true, transactionId: transaction.id };
}connection()#import { connection } from 'next/server';
import { fetchLiveRatesFromCentralBank } from '@/lib/finance';
export async function LiveExchangeRates() {
// await connection() явно указывает Dynamic I/O компилятору:
// "Этот компонент требует живого сетевого подключения клиента.
// Прерви статический пререндер (postpone) при сборке и исполни меня в рантайме".
await connection();
const rates = await fetchLiveRatesFromCentralBank();
return (
<div className="flex gap-4 p-3 rounded-lg border border-border bg-bg-surface font-mono text-xs">
<span className="text-text-muted">USD: <strong className="text-accent-cyan">{rates.USD}</strong></span>
<span className="text-text-muted">EUR: <strong className="text-accent-cyan">{rates.EUR}</strong></span>
<span className="text-text-muted">BTC: <strong className="text-accent-cyan">{rates.BTC}</strong></span>
</div>
);
}Компилятор встречает await connection(). На этапе сборки срабатывает postpone, формируя Dynamic Hole внутри Static Shell.
Статическая оболочка страницы собирается без задержек.
При подключении клиента сервер возобновляет рендеринг динамического слота и выполняет основную логику запроса.
Компонент получает доступ к актуальному сетевому контексту.
Вызовы after() регистрируют асинхронные задачи во внутреннем трекере контекста запроса (AsyncLocalStorage).
Клиентский поток не блокируется регистрацией.
Клиент получает HTTP-ответ и закрывает сокет. Сервер параллельно исполняет все задачи after() до их успешного завершения.
Процесс удерживается активным до завершения всех промисов.
Разработчик попытался вызвать cookies().set('theme', 'dark') внутри колбэка after(). Что произойдет?
Использование after() для тяжелых процессов (обработка видео, генерация отчетов на 5 минут, парсинг больших файлов) приведет к тому, что Serverless-функция завершится по системному таймауту (например, 15–30 секунд в Vercel или 60 секунд в AWS Lambda), оборвав операцию.
Решение: Для длительных задач (более 5–10 секунд) используйте специализированные очереди сообщений (BullMQ, Amazon SQS, RabbitMQ) и выделенные фоновые воркеры. after() предназначен для легковесных побочных эффектов: аудит, метрики, простые вебхуки.
Вызов методов установки куки (cookieStore.set()), перенаправления (redirect()) или модификации заголовков внутри функции after() приведет к исключению, так как сетевой сокет ответа уже закрыт.
Решение: Все мутации состояния клиента выполняйте в основном теле функции ДО вызова after().
Если асинхронный код внутри after() выбрасывает необработанную ошибку, хотя пользователь уже получил успешный ответ 200 OK, в логах сервера возникнет аварийный сигнал, способный загрязнить мониторинг ложными сбоями.
Решение: Всегда оборачивайте логику внутри after() в блок try/catch с явным логированием ошибок.