زمانی که با Next.js اپلیکیشن میسازیم، بحث بهینهسازی فقط به تقسیم کد یا lazy loading محدود نمیشود. در اینجا استفاده از ()after در Next.js اهمیت پیدا میکند؛ چون به ما کمک میکند تا مطمئن شویم سرور فقط کارهای ضروری را هنگام دریافت درخواست انجام میدهد و بقیه وظایف را به بعد از ارسال پاسخ موکول میکند. اینجاست که قابلیت جدید نسخه ۱۵ با نام ()after وارد عمل میشود.
تابع ()after یک API جدید در چرخه پردازش است که به ما اجازه میدهد منطقهای خاصی را بعد از پایان رندر و ارسال پاسخ اجرا کنیم، بدون اینکه تجربه کاربری کند شود. با این قابلیت دیگر نیازی به هکهای پیچیده در Server Actionها یا قرار دادن side effectها در Middleware نیست و همچنین نگرانی از کاهش سرعت Time To First Byte (TTFB) برای عملیاتهایی مثل ثبت لاگ یا ارسال دادههای آماری هم از بین میرود.
در این مقاله، به سراغ استفاده از ()after در Next.js میرویم تا ببینیم چطور میتوانیم اپلیکیشن را کارآمدتر، تمیزتر و مقیاسپذیرتر کنیم. بررسی خواهیم کرد ()after دقیقاً کجاها کاربرد دارد، در چه سناریوهایی کار نمیکند و رفتار آن هنگام تعامل با Server Componentها، Server Actionها و Route Handlerها چگونه است.
()after یک هوک جدید در چرخه روتینگ Next.js است که به ما امکان میدهد بخشی از کد را پس از ارسال پاسخ به کلاینت اجرا کنیم. این کار چرخه اصلی درخواست–پاسخ را مسدود نمیکند و برای وظایف غیرضروری مانند آنالیتیکس، ثبت لاگ، اجرای کارهای پسزمینه یا باطلسازی کش بسیار مناسب است.
میتوانیم ()after را مشابه دستور defer در هندلرهای HTTP زبان Go بدانیم؛ ابزاری برای اجرای منطقهای پاکسازی یا وظایف پسزمینه بعد از پایان کار اصلی. تفاوت اینجاست که ()after به صورت asynchronous و non-blocking عمل میکند و تضمینی درباره ترتیب اجرای کدها نمیدهد.
در معماری سنتی سرور، برای انجام کارهایی مثل بهروزرسانی شمارنده بازدید یا ارسال یک webhook، مجبور بودیم آنها را قبل از ارسال پاسخ اجرا کنیم یا از ابزارهایی مثل صف و Job پسزمینه استفاده کنیم. این روشها اغلب باعث کندی سیستم میشدند. اما حالا با استفاده از ()after در Next.js یک راهکار تمیز و یکپارچه داریم که میتوانیم این وظایف را مستقیماً درون route handlerها مدیریت کنیم.
تابع ()after در Next.js در چهار بخش مختلف قابل استفاده است:
اما رفتار آن در هر یک از این بخشها کاملاً یکسان نیست!
در Server Componentها، وظیفه ()after اجرای کد پس از رندر شدن و ارسال HTML به کاربر است. بنابراین اگر بخواهیم لاگ بگیریم، دادههای آماری جمع کنیم یا درخواستها را ردیابی نماییم بدون اینکه رندر صفحه کند شود، ()after بهترین گزینه است.
البته باید بدانیم که درون بلاک ()after نمیتوانیم از APIهای درخواست مثل cookies() یا headers() استفاده کنیم، چون ()after بعد از چرخه رندر React اجرا میشود و Next.js برای پشتیبانی از partial prerendering باید بداند کدام بخشها به این APIها وابستهاند.
مثلاً فرض کنید میخواهیم تعداد دفعات نمایش کامپوننت <Hero /> (یکی از اجزای کلیدی صفحه لندینگ) را ثبت کنیم. با استفاده از ()after در Next.js میتوانیم مطمئن شویم این لاگ فقط وقتی ثبت میشود که کامپوننت واقعاً رندر و برای کاربر ارسال شده باشد:
// app/page.tsx
import { after } from 'next/server';
import { logHeroImpression } from '@/lib/analytics';
export default function HomePage() {
const showHero = true; // maybe controlled by AB test or flag
if (showHero) {
after(() => {
logHeroImpression({ path: '/', experiment: 'hero-v2' });
});
}
return (
<main>
{showHero && <Hero />}
<OtherContent />
</main>
);
}
//lib/analytics.ts
import fs from 'fs';
import path from 'path';
type HeroImpressionData = {
path: string;
experiment: string;
timestamp?: string;
};
export function logHeroImpression({
path: routePath,
experiment,
timestamp = new Date().toISOString(),
}: HeroImpressionData) {
const logEntry = `[${timestamp}] Hero impression on route: "${routePath}", experiment: "${experiment}"\n`;
const logFilePath = path.join(process.cwd(), 'logs', 'hero-impressions.log');
try {
fs.mkdirSync(path.dirname(logFilePath), { recursive: true });
fs.appendFileSync(logFilePath, logEntry);
console.log('Hero impression logged');
} catch (err) {
console.error('Failed to log hero impression:', err);
}
}
در این حالت، بلاک ()after تنها زمانی اجرا میشود که <Hero /> واقعاً رندر شده باشد. اگر این کامپوننت نمایش داده نشود، هیچ لاگی ثبت نخواهد شد چون عملیات بعد از ارسال HTML انجام میشود. این یعنی هیچ اثری روی تجربه کاربر ندارد.
با این روش، دیگر نیازی به اسکریپتهای ردیابی سمت کلاینت یا ترفندهایی مثل useEffect نداریم. ردیابی روی سرور انجام میشود؛ جایی که هم تمیزتر و هم قابلاعتمادتر است، در مقایسه با جاوااسکریپت مرورگر که ممکن است دچار اختلال یا مسدود شود.
فقط باید توجه کنیم که اگر صفحه ما استاتیک رندر شده باشد (مثلاً با generateStaticParams)، تابع ()after در زمان build یا revalidation اجرا میشود و نه برای هر کاربر. بنابراین برای لاگگیری بر اساس درخواستهای واقعی کاربران، باید صفحه بهصورت داینامیک رندر شود:
export const dynamic = 'force-dynamic'
در Server Actionها، تابع ()after بعد از اجرای کامل اکشن و ارسال پاسخ به کلاینت اجرا میشود. همین ویژگی باعث میشود برای وظایف پسزمینهای که نباید فرآیند ارسال فرم یا بهروزرسانی UI را کند کنند، عالی باشد. نمونههای رایج شامل ارسال ایمیل، ثبت لاگ یا همگامسازی با APIهای خارجی هستند.
برخلاف Server Componentها، در Server Actionها دسترسی به context درخواست داریم. پس میتوانیم درون ()after از APIهایی مثل cookies() و headers() استفاده کنیم، البته با رعایت async/await.
مثلاً هنگام ساخت فرم ثبتنام، میتوانیم بعد از عضویت کاربر، ایمیل خوشآمدگویی را با استفاده از ()after در Next.js ارسال کنیم، بدون اینکه کاربر منتظر بماند:
'use server';
import { after } from 'next/server';
import { sendWelcomeEmail } from '@/lib/email';
import { db } from '@/lib/db';
export async function registerUser(formData: FormData) {
const email = formData.get('email') as string;
const user = await db.user.create({
data: { email },
});
after(async () => {
await sendWelcomeEmail(email);
});
return { success: true, userId: user.id };
}
در این مثال، پس از اینکه پاسخ به کلاینت ارسال شد، تابع sendWelcomeEmail() در پسزمینه اجرا میشود و کاربر پاسخ سریع و بیدرنگی در رابط کاربری مشاهده خواهد کرد، بدون اینکه درگیر تأخیر فرآیند ایمیل شود.
میتوانیم ()after را بهعنوان ثبت یک «واحد کار» تصور کنیم که Next.js آن را پس از پایان Server Action و در همان فرآیند سرور اجرا میکند. این ساختار وظیفه پسزمینه را از منطق اصلی جدا میکند و مانع از ایجاد تأخیر در پاسخ به کاربر میشود.
اینجا جایی است که تابع ()after بیشترین کارایی خود را نشان میدهد. در route handlerهایی مثل app/api/xyz/route.ts که مستقیم با Request و Response سر و کار داریم و نیاز به عملیاتهایی مثل لاگگیری، ثبت webhook یا تحلیل دادهها داریم، استفاده از ()after در Next.js بهترین انتخاب است.
در این حالت، ()after یک راهکار تمیز برای واگذاری وظایف غیرضروری به پسزمینه ارائه میدهد، بدون آنکه چرخه پاسخ را مسدود کند. یعنی دیگر لازم نیست side effectها را در منطق اصلی درخواست قرار دهیم یا نگران تأخیر در ارسال پاسخ باشیم.
برای نمونه، فرض کنید میخواهیم یک event خرید همراه با متادیتا ثبت کنیم. پس از تکمیل خرید توسط کاربر، با استفاده از ()after میتوانیم این دادهها را (مثل IP یا مرورگر کاربر) بعد از ارسال پاسخ ذخیره کنیم. به این ترتیب، کاربر سریعتر پاسخ میگیرد، در حالیکه سرور ما در پسزمینه اطلاعات را ثبت میکند:
// app/api/checkout/route.ts
import { after } from 'next/server';
import { headers } from 'next/headers';
import { logCheckoutEvent } from '@/lib/logging';
export async function POST(request: Request) {
const body = await request.json();
const order = await createOrder(body);
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown';
const ip = (await headers()).get('x-forwarded-for') || 'unknown';
await logCheckoutEvent({
orderId: order.id,
userAgent,
ip,
timestamp: new Date().toISOString(),
});
});
return Response.json({ success: true, orderId: order.id });
}
در Middleware هم میتوانیم از ()after استفاده کنیم. این تابع بعد از ارسال پاسخ اجرا میشود و برای side effectهای سبک و غیرمسدودکننده بسیار مناسب است؛ مثل لاگگیری، افزودن برچسب یا پایش پسزمینه. برخلاف route handlerها، در اینجا نمیتوانیم محتوای پاسخ را تغییر دهیم، چون ()after در Middleware فقط برای عملیات جانبی طراحی شده است.
برای مثال، اگر بخواهیم همه درخواستهای ورودی اپلیکیشن را لاگ کنیم، کافی است این کار را در Middleware بنویسیم. با ورود هر درخواست، Middleware آن را بررسی میکند، ادامه مسیر را با NextResponse.next() میدهد، و سپس ()after متادیتا مثل URL، آدرس IP و مرورگر را در پسزمینه ثبت میکند. این روش هم سادهتر است و هم عملکرد برنامه را تحت تأثیر قرار نمیدهد.
// middleware.ts
import { NextResponse } from 'next/server';
import { after } from 'next/server';
import { logRequest } from '@/lib/traffic';
export function middleware(request: Request) {
const response = NextResponse.next();
const url = request.url;
const ip = request.headers.get('x-forwarded-for') || 'unknown';
const userAgent = request.headers.get('user-agent') || 'unknown';
after(async () => {
await logRequest({ url, ip, userAgent, timestamp: new Date().toISOString() });
});
return response;
}
تابع ()after قابلیت اجرا بهصورت تودرتو را هم دارد. یعنی میتوانیم چندین ()after در لایههای مختلف (مثل layout، page یا کامپوننت) داشته باشیم. در این حالت همه آنها اجرا میشوند، اما به ترتیب معکوس رندر شدنشان. یعنی اول داخلیترین کامپوننت اجرا میشود، بعد parent و همینطور به بالا.
این ساختار شبیه یک stack با مدل Last-In-First-Out است. برخلاف Middleware که از بالا به پایین اجرا میشود، ()after مثل یک پشته پاکسازی عمل میکند که هر لایه وظیفه خودش را push کرده و بعد از ارسال پاسخ، از درون به بیرون اجرا میشود.
برای مثال:
ما میخواهیم همه اینها ثبت شوند، اما اولویت با جزئیترها باشد. این همان چیزی است که ()after برایمان فراهم میکند.
// app/layout.tsx
import { after } from 'next/server';
export default function RootLayout({ children }) {
after(() => console.log('[AFTER] root layout'));
return <html><body>{children}</body></html>;
}
// app/product/layout.tsx
import { after } from 'next/server';
export default function ProductLayout({ children }) {
after(() => console.log('[AFTER] product layout'));
return <section>{children}</section>;
}
// app/product/page.tsx
import { after } from 'next/server';
export default function ProductPage() {
after(() => console.log('[AFTER] product page'));
return <h1>Product</h1>;
}
اگر کاربری به این صفحه وارد شود، ترتیب لاگها به این صورت در لاگهای سرور ظاهر خواهد شد:
[AFTER] product page [AFTER] product layout [AFTER] root layout
بنابراین بله، ()afterها بهصورت cascade اجرا میشوند، اما نه از بالا به پایین مثل middlewareها، بلکه از پایین به بالا، از درونیترین کامپوننتی که رندر شده است.
نکته مهم دیگر این است که این موضوع با استفاده چندباره از ()after در یک فایل یا یک کامپوننت فرق دارد. اگر چند بار درون یک کامپوننت ()after را صدا بزنیم، آنها نیز به ترتیب معکوس اجرا میشوند، اما درون همان scope باقی میمانند و به layoutهای اطراف اهمیتی نمیدهند.
یکی از دغدغههای اصلی هنگام استفاده از ()after در Next.js این است که اگر خطا رخ دهد، آیا باز هم اجرا میشود یا نه؟
مثلاً:
500 ارسال شود چه؟طبق مستندات Next.js، حتی اگر پاسخ کامل ارسال نشود یا خطایی رخ دهد، ()after باز هم اجرا میشود. این ویژگی باعث اعتماد بیشتر به آن میشود؛ چون میتوانیم مطمئن باشیم لاگگیری، تحلیل خطا یا ذخیره متادیتا همیشه انجام خواهد شد.
برای نمونه، حتی اگر در مسیر /api/error-test خطایی throw شود، باز هم callback تعریفشده در ()after اجرا میشود. این یعنی ما هیچوقت اطلاعات مهمی را به خاطر خطا از دست نمیدهیم.
// app/api/error-test/route.ts
import { NextResponse } from 'next/server';
import { after } from 'next/server';
export async function GET() {
const userId = 'abc123';
after(() => {
console.log('[AFTER] Logging userId:', userId);
});
throw new Error('Simulated failure');
}
در این حالت، زمانی که به /api/error-test دسترسی پیدا کنیم، یک پاسخ 500 دریافت میکنیم، اما همچنان لاگ سرور ما این پیام را چاپ میکند:
[AFTER] Logging userId: abc123
در اپلیکیشنهای واقعی، خطاها اجتنابناپذیرند: ممکن است مسیرها کرش کنند، APIها از کار بیفتند یا کامپوننتها دچار مشکل شوند. در چنین شرایطی باید بدانیم چه چیزی باعث بروز خطا شده است: کاربر چه کسی بوده؟ چه ورودیای داده؟ در حال انجام چه عملی بوده؟
با استفاده از ()after در Next.js میتوانیم همه این اطلاعات را ثبت کنیم، حتی اگر مسیر کرش کند. بدون این قابلیت، مجبور بودیم در هر مسیر یک بلاک try...catch بنویسیم، لاگگیری را دستی انجام دهیم و دوباره خطا را throw کنیم. این رویکرد هم شکننده است، هم پرخطا و سخت برای نگهداری. اما با ()after میتوانیم فرآیند لاگگیری خطاها را متمرکز و ساده کنیم. این ویژگی مثل یک شبکه ایمنی مطمئن عمل میکند؛ چه درخواست با موفقیت کامل شود، چه وسط راه دچار مشکل شود!
البته باید دقت کنیم: اینکه ()after هنگام خطا اجرا میشود به این معنا نیست که به همه دادهها دسترسی دارد. اگر قبل از خطا اطلاعاتی مثل header یا cookie را نگرفته باشیم، در ()after هم در دسترس نخواهند بود. پس اگر قرار است دادههایی مثل IP یا user agent را در منطق ()after استفاده کنیم، باید آنها را زودتر و قبل از بروز خطا استخراج کنیم:
const ip = request.headers.get('x-forwarded-for');
after(() => logErrorWithIP(ip));
با این رویکرد، حتی اگر بخش اصلی handler خطا بدهد، دادههای لاگ شده همچنان ذخیره میشوند و از دست نمیروند.
قبل از اینکه از ()after استفاده کنیم، چند نکته کلیدی وجود دارد:
()after نه یک هوک است و نه در زمان اجرا واکنشی دارد. برخلاف هوکهای React، این تابع با تغییر props یا state دوباره اجرا نمیشود. فقط یکبار اجرا میشود: در حین رندر سمت سرور و پس از ارسال پاسخ.
اگر بخواهیم کدی را شرطی اجرا کنیم، باید این شرط را خودمان مدیریت کنیم؛ مثلاً فراخوانی ()after را در یک if قرار دهیم.
اگر داخل ()after تابعهایی با هزینه بالا (مثل خواندن از دیتابیس یا فراخوانی APIها) داشته باشیم، بهتر است آنها را داخل React.cache() قرار دهیم. این کار باعث میشود نتیجه تابع فقط یک بار در هر درخواست محاسبه شود و در صورت استفاده در چند ()after تکراری، دوباره اجرا نشود. این تکنیک مخصوصاً وقتی مفید است که ()after در چند کامپوننت تودرتو استفاده شده باشد.
import { cache } from 'react';
const getUser = cache(async (id) => {
return db.user.findUnique({ where: { id } });
});
()after یک static export است؛ یعنی در هر درخواست فقط یکبار روی سرور اجرا میشود. این تابع به تغییرات state یا props واکنش نشان نمیدهد و به تعاملات کاربر هم پاسخگو نیست. بنابراین نمیتوانیم در آن از هوک useState یا هوک useEffect استفاده کنیم.
استفاده از ()after در Next.js فقط در دایرکتوری app ممکن است. در دایرکتوریهای قدیمی مثل pages یا pages/api کار نمیکند. همچنین ()after فقط روی سرور اجرا میشود و در کلاینت قابل استفاده نیست. اگر در یک کامپوننت کلاینتی از آن استفاده کنیم، خطا خواهیم گرفت.
برای اجرای منطق پس از ارسال پاسخ، ابزارهای دیگری هم وجود دارد. انتخاب درست به محل استفاده بستگی دارد:
waitUntil(): مخصوص Edge Middleware است. اجازه میدهد وظایف پسزمینه بعد از بازگشت پاسخ اجرا شوند. شبیه ()after عمل میکند اما فقط در Edge Runtime.await: اگر وظیفهای (مثل ذخیره در دیتابیس یا پردازش پرداخت) باید قبل از ارسال پاسخ انجام شود، باید از await استفاده کنیم تا پاسخ معطل بماند.اما وقتی هدفمان اجرای side effectها بعد از پاسخ است مثل:
استفاده از ()after در Next.js بهترین گزینه میباشد.
تابع ()after در Next.js یک گام مهم برای ساخت اپلیکیشنهای مقیاسپذیر و سریع است. این ابزار یک روش native برای اجرای عملیات غیرضروری پس از ارسال پاسخ فراهم میکند و جلوی معطل ماندن کاربر برای اجرای side effectها را میگیرد.
راز استفاده مؤثر از ()after این است که محدودیتهای آن را بشناسیم و جای درست از آن استفاده کنیم. چون بسته به موقعیت (route، middleware یا layout) تواناییهای متفاوتی دارد. وقتی درست استفاده شود، ()after میتواند تجربه کاربری را سریعتر کند و کیفیت اپلیکیشن را از دید کاربر به شکل محسوسی بالا ببرد.