چگونه TTFB وباپلیکیشن صرافی کاریزما را به زیر ۸۵ میلیثانیه کاهش دادیم؟
بررسی استراتژیهای کشینگ توزیعشده با Redis و معماری Server Components در Next.js برای مدیریت ترافیک سنگین ساعات پیک معاملاتی بورس.
مهندس مهرداد کبیریمهندس ارشد بکاند و زیرساخت
صرافی کاریزما در ساعات پیک معاملات، ترافیکی ۱۲ برابرِ حالت عادی را تجربه میکند؛ درست همان ساعاتی که هر ۱۰۰ میلیثانیه تأخیر یعنی معاملهگران به رقیب میروند. در این مقاله مسیر کاهش TTFB از ۶۴۰ میلیثانیه به زیر ۸۵ میلیثانیه را شرح میدهیم.
تشخیص: تأخیر از کجا میآمد
اولین قدم، اندازهگیری خام بود — بدون حدس زدن. با یک ترافیکسنجه سبک روی سه لایه، نقشه تأخیر را ساختیم:
- ۲۹۰ms: رندر سمت سرورِ صفحههایی که داده قیمت را بهصورت مستقیم از سرویس داخلی میگرفتند.
- ۱۸۰ms: رفتوبرگشت تا پایگاهداده برای ساخت لیست سفارشها (N+1 در کوئریها).
- ۱۲۰ms: سریالایز شدن JSON سنگین و Gzip کند روی CPU اشتراکی.
اندازهگیری قبل از بهینهسازی، یک شعار نیست؛ تنها راهی است که مطمئن شوید کدی را که تأخیر نمیسازد دست نمیزنید.
لایه اول: کشینگ توزیعشده با Redis
دادههای قیمت ذاتاً stale هستند؛ پس کش ۵ ثانیهای نهتنها بلامانع، بلکه ضروری است. الگوی stale-while-revalidate را با یک لایه نازک روی Redis پیاده کردیم:
import { redis } from "@/server/redis";
const PRICE_TTL = 5; // ثانیه — داده قیمت زنده است
export async function getOrderBook(symbol: string) {
const key = `ob:${symbol}`;
const cached = await redis.get(key);
if (cached) {
void refreshInBackground(symbol, key); // بازاعتبارسنجی غیرمسدودکننده
return JSON.parse(cached) as OrderBook;
}
const fresh = await fetchOrderBook(symbol);
await redis.set(key, JSON.stringify(fresh), { EX: PRICE_TTL });
return fresh;
}
async function refreshInBackground(symbol: string, key: string) {
const fresh = await fetchOrderBook(symbol);
await redis.set(key, JSON.stringify(fresh), { EX: PRICE_TTL });
}
نکته ظریف این الگو این است: درخواستهای گرم هرگز منتظر پایگاهداده نمیمانند؛ فقط دسته اولِ پس از expire یکبار تأخیر کامل را میپذیرد.
لایه دوم: Server Components و حذف دادههای مرده
با مهاجرت صفحات بازار به Next.js App Router، دادههایی که فقط در hydration لازم بودند از رندر سرور حذف شدند و Client Componentها به حداقل رسیدند:
- لیست سفارشها کاملاً سمت سرور رندر میشود و فقط «تیکر» قیمت کلاینت است.
- اسکریپتهای تحلیلی با
asyncاز مسیر بحرانی خارج شدند. - پاسخهای API با
immutable, max-age=60در لبه کش میشوند.
لایه سوم: سریالایز و فشردهسازی
خروجی JSON صفحه اصلی از ۳۴۰KB به ۹۶KB رسید: فیلدهای بلااستفاده حذف و اعداد بهجای رشته («۱۲۵۰۰» ← 12500) سریال شدند. در لبه هم بهجای Gzip از Brotli با کیفیت ۶ استفاده کردیم.
| سناریو | TTFB | حجم HTML | LCP |
|---|---|---|---|
| قبل از بهینهسازی | ۶۴۰ms | ۳۴۰KB | ۳.۱s |
| + کش Redis | ۲۱۰ms | ۳۴۰KB | ۲.۴s |
| + Server Components | ۱۱۰ms | ۱۸۰KB | ۱.۶s |
| + سریالایز/Brotli | ۸۵ms | ۹۶KB | ۱.۲s |
نتیجهگیری
TTFB زیر ۸۵ میلیثانیه نتیجه سه تصمیم متوالی است، نه یک ترفند: اندازهگیری صادقانه، کش در جای درست، و حذف دادهای که در رندر اول لازم نیست. اگر یک چیز از این پروژه بردارید، همان ترافیکسنجه اول باشد؛ چون بدون آن، بقیه مسیر حدس و گمان است.
برچسبها
مهندس مهرداد کبیری
مهندس ارشد بکاند و زیرساختدرباره نویسنده
تخصص در معماریهای توزیعشده، کشینگ سطح لبه و بهینهسازی عملکرد وباپلیکیشنهای پرمصرف در فینتک.
مقالات نویسنده

