استراتژی‌های کش HTTP در ۲۰۲۶: راهنمای عملی Cache-Control، stale-while-revalidate و ابطال کش

راهنمای عملی چیدمان Cache-Control، stale-while-revalidate، ETag و ابطال کش تگ‌محور در ۲۰۲۶ با نمونه‌های واقعی برای Next.js 15، Cloudflare و Vercel که TTFB را زیر ۵۰ میلی‌ثانیه می‌برد.

راهنمای کش HTTP و SWR ۲۰۲۶

به‌روزرسانی: ۱۸ سپتامبر ۲۰۲۶

استراتژی کش HTTP در سال ۲۰۲۶ یعنی ترکیب سرآیند Cache-Control با دستورالعمل‌های stale-while-revalidate و stale-if-error، اعتبارسنجی شرطی از طریق ETag، و یک لایه ابطال کش تگ‌محور روی CDN که پاسخ‌های تازه را در چند ثانیه در سراسر شبکه پخش می‌کند. اگر این چهار قطعه درست کنار هم بنشینند، TTFB بیشتر درخواست‌ها به زیر ۵۰ میلی‌ثانیه می‌رسد، بار Origin تا ۹۵٪ کاهش پیدا می‌کند و کاربران هیچ‌گاه محتوای منقضی نمی‌بینند. این راهنما همان چیدمانی است که در پروژه‌های واقعی جواب داده و به‌جای تئوری، نمونه‌های اجرایی برای Cloudflare، Vercel و Next.js می‌آورد.

  • سرآیند Cache-Control با ترکیب max-age، s-maxage، stale-while-revalidate و immutable عملاً تمام رفتار کش مرورگر و CDN را کنترل می‌کند.
  • الگوی stale-while-revalidate پاسخ بیات را در پس‌زمینه به کاربر می‌دهد و همزمان نسخه تازه را می‌گیرد؛ نتیجه‌اش تجربه‌ای بدون انتظار حتی هنگام رفرش است.
  • ابطال کش تگ‌محور (Cache Tag Purge) در Cloudflare، Fastly و Next.js `revalidateTag` جایگزین مدرن TTLهای کوتاه است و از دیدن محتوای بیات بعد از انتشار جلوگیری می‌کند.
  • ترکیب ETag با پاسخ‌های 304 Not Modified پهنای باند را برای دارایی‌های پویا تا ۸۰٪ کم می‌کند، حتی وقتی محتوا کش‌پذیر نیست.
  • در ۲۰۲۶ همه مرورگرهای اصلی از stale-while-revalidate پشتیبانی می‌کنند و مشخصات RFC 9111 استاندارد رفتار CDNها شده است.

Cache-Control چیست و چگونه کار می‌کند؟

سرآیند Cache-Control قراردادی بین سرور، مرورگر و هر واسطه‌ای در مسیر (پروکسی، CDN، Service Worker) است که مشخص می‌کند پاسخ چقدر و توسط چه کسانی قابل کش شدن است. این سرآیند در RFC 9111 استاندارد HTTP Caching تعریف شده و در ۲۰۲۶ عملاً تنها منبع حقیقت برای رفتار کش وب است. سرآیندهای قدیمی‌تر مثل Expires و Pragma صرفاً برای سازگاری با کلاینت‌های خیلی قدیمی نگه داشته می‌شوند و اولویتشان از Cache-Control پایین‌تر است.

در تجربه من، مهم‌ترین اشتباه تیم‌ها این است که Cache-Control را فقط با max-age می‌نویسند و بقیه ابزارهای این سرآیند را نادیده می‌گیرند. یک هدر خوب مثل نمونه زیر است:

Cache-Control: public, max-age=60, s-maxage=3600, stale-while-revalidate=86400, stale-if-error=604800

این پیکربندی می‌گوید: مرورگر ۶۰ ثانیه پاسخ را تازه بداند، CDN تا یک ساعت آن را تازه نگه دارد، تا یک روز بعد از انقضا مجاز است پاسخ بیات را ارائه کند در حالی که در پس‌زمینه تازه‌سازی می‌کند، و اگر Origin در دسترس نبود تا یک هفته پاسخ بیات را تحویل بدهد. همه این‌ها فقط با پنج دستورالعمل. برای شناخت کامل دستورالعمل‌ها به مستندات MDN درباره Cache-Control مراجعه کنید که فهرست کاملی از تمام directiveها را ارائه می‌دهد.

public در مقابل private

دستور public اجازه می‌دهد هر واسطه‌ای پاسخ را کش کند، در حالی که private فقط به کش خود کاربر (مرورگر) اجازه می‌دهد. برای پاسخ‌های شخصی‌سازی‌شده حتماً private بگذارید، وگرنه ممکن است داشبورد یک کاربر روی CDN کش شود و به کاربر دیگری نمایش داده شود.

no-store، no-cache و must-revalidate

این سه دستور به اشتباه به‌جای هم به‌کار می‌روند. no-store یعنی هیچ لایه‌ای اجازه ذخیره ندارد؛ no-cache یعنی ذخیره مجاز است ولی قبل از هر بار استفاده باید با سرور اعتبارسنجی شود؛ و must-revalidate یعنی بعد از انقضا حتماً باید اعتبارسنجی شود. برای فرم‌های ورود از no-store، برای HTML صفحات پویا از no-cache، و برای دارایی‌های نیمه‌ایستا از max-age بلند به همراه must-revalidate استفاده کنید.

تفاوت max-age و s-maxage چیست؟

تفاوت اصلی این است که max-age برای هر کشی معتبر است، ولی s-maxage فقط برای کش‌های اشتراکی (پروکسی، CDN، لود بالانسر) اعمال می‌شود و آن‌ها را وادار می‌کند مقدار max-age را نادیده بگیرند. این تفاوت روی الگوهای پیشرفته کش مؤثر است چون معمولاً می‌خواهیم مرورگر پاسخ را کمتر نگه دارد ولی CDN بیشتر.

یک مثال واقعی: صفحه محصول یک فروشگاه اینترنتی. اگر max-age=3600 بگذارید، مرورگر کاربر تا یک ساعت نسخه قدیمی را نشان می‌دهد و رفرش‌های او نمی‌توانند نسخه جدید را بگیرند. برعکس، max-age=0, s-maxage=3600 باعث می‌شود مرورگر همیشه از CDN بپرسد و CDN تا یک ساعت خودش پاسخ می‌دهد. نتیجه: تازگی برای کاربر، صرفه‌جویی برای Origin.

# مناسب برای صفحات پویا با CDN جلویی
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400

# مناسب برای دارایی‌های ثابت با هش در نام فایل
Cache-Control: public, max-age=31536000, immutable

Age و Vary: دو دستیار مهم

سرآیند Age که CDN به پاسخ اضافه می‌کند نشان می‌دهد این پاسخ چند ثانیه در کش بوده است. برای دیباگ کش HTTP اولین چیزی است که باید نگاه کنید. سرآیند Vary نیز به کش می‌گوید که پاسخ بر اساس چه سرآیند درخواستی (مثل Accept-Encoding یا Accept-Language) متفاوت است. اگر Vary: Cookie بگذارید عملاً کش را غیرفعال می‌کنید؛ این یکی از رایج‌ترین شکارگاه‌های عملکردی است.

stale-while-revalidate چگونه کار می‌کند؟

دستورالعمل stale-while-revalidate (که به اختصار SWR می‌گویند) به کش اجازه می‌دهد پاسخ منقضی‌شده را بلافاصله تحویل کاربر بدهد و همزمان در پس‌زمینه یک درخواست به Origin بفرستد تا نسخه تازه را جایگزین کند. این الگو در RFC 5861 استاندارد stale response تعریف شده و از سال ۲۰۲۲ تمام مرورگرهای اصلی، Cloudflare، Fastly، Akamai و Vercel از آن پشتیبانی می‌کنند.

مثلاً با هدر Cache-Control: max-age=10, stale-while-revalidate=60:

  • ثانیه ۰ تا ۱۰: کش پاسخ تازه را برمی‌گرداند، هیچ درخواستی به Origin نمی‌رود.
  • ثانیه ۱۰ تا ۷۰: کش هنوز پاسخ قدیمی را می‌دهد ولی همزمان یک درخواست async به Origin می‌فرستد. کاربر منتظر نمی‌ماند.
  • بعد از ثانیه ۷۰: کش دیگر پاسخ بیات نمی‌دهد؛ اولین درخواست منتظر می‌ماند تا Origin پاسخ تازه بدهد.

این ترکیب معجزه می‌کند برای APIهایی که نرخ درخواست بالا و تازگی نسبی می‌خواهند. من روی داشبوردهای تحلیلی max-age=5, stale-while-revalidate=300 می‌گذارم؛ نتیجه این است که ۹۹٪ درخواست‌ها زیر ۱۰ میلی‌ثانیه پاسخ می‌گیرند و همزمان داده هرگز بیش از پنج ثانیه قدیمی نیست.

ترکیب با stale-if-error

خواهر دوقلوی SWR، دستورالعمل stale-if-error است که می‌گوید اگر Origin در دسترس نبود یا خطای 5xx برگرداند، تا این مدت زمان اجازه داری پاسخ بیات را بدهی. این ابزار برای مقاومت در برابر Outage عالی است. یک ترکیب نمونه در پروژه‌های تولیدی من:

Cache-Control: public, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400

یعنی: در حالت عادی هر یک دقیقه تازه‌سازی، اگر Origin کند بود تا ۱۰ دقیقه به‌آرامی refresh، و اگر Origin کلاً افتاد تا ۲۴ ساعت پاسخ بده. این یک شبکه ایمنی بسیار قوی برای بحران‌های شبکه است. اگر می‌خواهید در کنار این استراتژی، TTFB را حتی پایین‌تر ببرید، به راهنمای بهینه‌سازی TTFB با Edge Computing نگاه کنید که چیدمان کامل Origin به لبه را پوشش می‌دهد.

ETag و اعتبارسنجی شرطی

سرآیند ETag امضایی است که سرور به هر نسخه از یک منبع می‌چسباند؛ می‌تواند هش محتوا، نسخه دیتابیس یا هر شناسه یکتای دیگری باشد. وقتی مرورگر پاسخ منقضی می‌شود، به‌جای گرفتن دوباره کل پاسخ، درخواستی با سرآیند If-None-Match می‌فرستد. اگر ETag تغییر نکرده باشد، سرور 304 Not Modified بدون بدنه برمی‌گرداند و مرورگر همان کپی محلی را تازه اعلام می‌کند.

دو نوع ETag وجود دارد: strong (مثل "abc123") که یعنی محتوا دقیقاً بایت‌به‌بایت یکسان است، و weak (مثل W/"abc123") که یعنی از نظر معنایی معادل است. برای اکثر APIها weak ETag کافی است چون فشرده‌سازی یا فرمت‌بندی جزئی ممکن است تغییر کند بدون آنکه محتوا واقعاً عوض شود.

# پاسخ اول
HTTP/2 200 OK
ETag: W/"user-42-v17"
Cache-Control: private, max-age=0, must-revalidate

# درخواست بعدی
GET /api/user/42
If-None-Match: W/"user-42-v17"

# پاسخ اگر تغییر نکرده باشد
HTTP/2 304 Not Modified

Last-Modified: جایگزین سبک‌تر

وقتی محاسبه ETag سنگین است، می‌توانید از Last-Modified با If-Modified-Since استفاده کنید. دقتش ثانیه‌ای است (بر خلاف ETag که می‌تواند هر تغییری را ثبت کند) ولی برای فایل‌های ایستا کاملاً کافی است. سرورهای Nginx و Cloudflare به‌صورت پیش‌فرض این را برای فایل‌های ایستا اضافه می‌کنند.

استراتژی‌های ابطال کش در CDN

ابطال کش یعنی وقتی محتوا تغییر می‌کند، به CDN بگویی نسخه قدیمی را دور بریزد. این بخش واقعاً سخت کار است؛ همان جمله معروف Phil Karlton که «تنها دو مشکل سخت در علوم کامپیوتر وجود دارد: ابطال کش و نام‌گذاری متغیرها». در ۲۰۲۶ سه استراتژی اصلی بر همه چیره است:

۱. Purge بر اساس URL

ساده‌ترین حالت: به API CDN می‌گویی /api/products/42 را از کش پاک کن. برای سایت‌های کوچک کافی است ولی وقتی یک محصول در ده صفحه لیستینگ نشان داده می‌شود، باید همه ده URL را پاک کنی؛ نگهداری این نگاشت به‌سرعت غیرقابل مدیریت می‌شود.

۲. Purge بر اساس تگ (Surrogate Key)

هنگام serve کردن پاسخ، سرآیند Cache-Tag (در Cloudflare) یا Surrogate-Key (در Fastly) اضافه می‌کنی که تگ‌های مربوط به آن پاسخ را اعلام کند. سپس هنگام تغییر، به CDN می‌گویی «هر چیزی با تگ product-42 را باطل کن». این یک تغییر ذهنی بزرگ است:

// در Origin هنگام پاسخ به صفحه محصول
res.setHeader('Cache-Tag', 'product-42,category-shoes,brand-nike');

// هنگام آپدیت محصول ۴۲ (فراخوانی به Cloudflare)
await fetch('https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${CF_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ tags: ['product-42'] })
});

حالا هر صفحه‌ای که این محصول در آن ظاهر می‌شد، در چند ثانیه در سراسر شبکه CDN باطل می‌شود. من در پروژه‌های تجاری همیشه تگ‌ها را با یک قرارداد ثابت طراحی می‌کنم: <entity>-<id> برای موجودیت‌ها و list-<entity> برای صفحات لیستینگ. این چیدمان ذهنی، توسعه‌دهنده‌های تازه‌کار را هم قادر می‌سازد پیامدهای ابطال را دقیق پیش‌بینی کنند.

۳. Soft Purge با TTL کوتاه

اگر CDN شما تگ ندارد، می‌توانید TTL را ۳۰-۶۰ ثانیه بگذارید و به stale-while-revalidate بلند تکیه کنید. پاسخ‌ها به‌سرعت refresh می‌شوند ولی همچنان از هجوم مستقیم به Origin در امانند. این الگو در Vercel Edge Cache و Netlify به‌طور پیش‌فرض استفاده می‌شود.

پیاده‌سازی در Next.js و فریمورک‌های مدرن

در ۲۰۲۶ فریمورک‌های مدرن انتزاعی روی Cache-Control ساخته‌اند که برای اکثر تیم‌ها راحت‌تر است. در Next.js 15 (App Router)، تابع‌های fetch با آپشن next: { revalidate, tags } کش می‌شوند و بعد با revalidateTag ابطال می‌شوند:

// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: {
      revalidate: 3600,          // ISR: هر یک ساعت خودکار
      tags: [`product-${params.id}`, 'products']
    }
  }).then(r => r.json());

  return <ProductView product={product} />;
}

// app/api/webhook/product-updated/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  const { productId } = await req.json();
  revalidateTag(`product-${productId}`);
  return Response.json({ revalidated: true });
}

در پس‌زمینه، Next.js دقیقاً همان الگوی Cache-Tag را با CDN‌های سازگار (Vercel، Cloudflare) به کار می‌گیرد. مزیت این است که بدون کد سفارشی، هم Origin کش دارد، هم CDN، و هم مرورگر. اگر روی همین جریان، JavaScript کلاینت را هم سبک نگه دارید (که کار جداگانه‌ای است)، تجربه واقعاً سریع می‌شود؛ در راهنمای بهینه‌سازی باندل جاوااسکریپت جزئیات تکنیک‌های Tree Shaking و Code Splitting برای این هدف را آورده‌ام.

Service Worker: کش لایه چهارم

Service Worker یک لایه کش اضافی روی مرورگر است که کاملاً پروگرام‌پذیر است. برای APIهای offline-first یا shell پیشرو، الگوی «Cache First, Network Fallback» یا «Network First, Cache Fallback» را با چند خط پیاده می‌کنید:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/static/')) {
    // Cache first (به‌روزرسانی در پس‌زمینه)
    event.respondWith(
      caches.match(event.request).then(cached => {
        const fetchPromise = fetch(event.request).then(res => {
          caches.open('api-v1').then(c => c.put(event.request, res.clone()));
          return res;
        });
        return cached || fetchPromise;
      })
    );
  }
});

این الگو در عمل همان stale-while-revalidate است ولی داخل مرورگر اجرا می‌شود، حتی وقتی کاربر آفلاین است. برای PWAها یا SPAهایی که تجربه offline می‌خواهند، جایگزینی ندارد.

اشتباهات رایج و بهترین شیوه‌ها

وقتی کد کش‌گذاری تیم‌های مختلف را مرور می‌کنم، هفت اشتباه بارها تکرار می‌شود. رفع همین هفت مورد معمولاً TTFB را نصف می‌کند بدون هیچ تغییر معماری:

۱. تنظیم Vary: * یا Vary: Cookie

این عملاً کش را غیرفعال می‌کند چون هر کاربر با کوکی متفاوت به یک نسخه جدا در کش نیاز دارد. برای صفحات authenticated، بهتر است Cache-Control: private بگذارید و از Vary: Cookie پرهیز کنید.

۲. TTL طولانی روی HTML بدون سازوکار ابطال

اگر max-age=3600 روی HTML صفحه بگذارید و ابطال کش تنظیم نکنید، بعد از هر deploy کاربران تا یک ساعت نسخه قدیمی را می‌بینند. راه‌حل: TTL کوتاه (< 60 ثانیه) روی HTML + Cache-Tag برای ابطال دستی.

۳. فراموش کردن immutable روی دارایی‌های هش‌دار

مرورگر بدون immutable هنگام رفرش کاربر، درخواست conditional برای همه فایل‌های JS/CSS می‌فرستد. با immutable، این درخواست‌ها حذف می‌شوند و صفحه چند صد میلی‌ثانیه سریع‌تر بارگذاری می‌شود. برای درک بهتر تأثیر کش روی بارگذاری تصاویر، راهنمای بهینه‌سازی مدرن تصاویر AVIF و WebP نکات مکملی دارد.

۴. اعتماد به Cloudflare/Vercel بدون هدر صریح

CDNها هدرهای پیش‌فرض دارند ولی این پیش‌فرض‌ها بین provider ها متفاوت است و می‌تواند تغییر کند. همیشه Cache-Control را در Origin به‌صراحت تنظیم کنید تا رفتار قابل پیش‌بینی باشد.

۵. نادیده گرفتن Age در دیباگ

وقتی گزارش می‌شود «چرا داده تازه نیست؟»، اول از همه به سرآیند Age پاسخ نگاه کنید. مقدار بالای Age نشان می‌دهد پاسخ در CDN نگه داشته می‌شود؛ باید ببینید TTL درست تنظیم شده یا ابطال کار نمی‌کند.

۶. کش‌کردن پاسخ‌های 5xx

اگر stale-if-error تنظیم نکرده‌اید و CDN شما پاسخ‌های 500 را کش می‌کند، یک خطای گذرا می‌تواند ساعت‌ها روی سایت باقی بماند. بیشتر CDNها به‌طور پیش‌فرض 5xx را کش نمی‌کنند، ولی حتماً پیکربندی خود را چک کنید.

۷. مخلوط کردن Session با URL کش‌پذیر

پارامتر sessionید در URL، هر session یک نسخه جدید در کش می‌سازد و به‌سرعت storage CDN را پر می‌کند. session را در کوکی یا header نگه دارید و از URL بیرون بیاورید.

سوالات متداول

Cache-Control چه تفاوتی با Expires دارد؟

سرآیند Expires یک تاریخ مطلق است (مثلاً «۲۰۲۶-۱۰-۰۱ ساعت ۱۲»)، در حالی که Cache-Control: max-age مدت زمان نسبی از لحظه دریافت پاسخ است. اگر ساعت سرور و کلاینت هماهنگ نباشد، Expires اشتباه کار می‌کند؛ به همین دلیل Cache-Control اولویت دارد و طبق RFC 9111 در حضور هر دو، Cache-Control معتبر است.

آیا stale-while-revalidate در همه مرورگرها پشتیبانی می‌شود؟

بله، در ۲۰۲۶ تمام مرورگرهای اصلی (Chrome، Firefox، Safari، Edge) از stale-while-revalidate پشتیبانی کامل دارند. Safari از نسخه ۱۴ و Firefox از نسخه ۹۹ آن را افزوده‌اند. تمام CDN‌های اصلی مثل Cloudflare، Fastly، Akamai و Vercel هم آن را در لایه edge پیاده کرده‌اند.

چه زمانی باید از ETag استفاده کنم و چه زمانی از Last-Modified؟

اگر محتوا می‌تواند در همان ثانیه چند بار تغییر کند (مثل API‌های شخصی‌سازی‌شده)، ETag دقیق‌تر است. برای فایل‌های ایستا یا داده‌هایی که فقط چند بار در روز تغییر می‌کنند، Last-Modified سبک‌تر است چون به تولید هش نیاز ندارد. می‌توانید هر دو را همزمان بفرستید؛ مرورگر ETag را ترجیح می‌دهد.

چگونه کش CDN را بعد از deploy ابطال کنم؟

بهترین راه در ۲۰۲۶ استفاده از Cache Tags است: هنگام serve پاسخ، سرآیند Cache-Tag با شناسه یکتای deploy (مثلاً commit hash) اضافه کنید، و بعد از deploy فقط تگ‌های مربوط را purge کنید. این کار در Cloudflare و Fastly مقیاس‌پذیر است و برخلاف purge-by-URL نگهداری نمی‌طلبد.

آیا استفاده از immutable ریسک دارد؟

فقط وقتی که نام فایل شامل هش محتوا نباشد. اگر نام فایل به‌صورت main.js است و بدون تغییر نام آن را re-deploy کنید، مرورگرهای کاربران که هدر immutable را دیده‌اند می‌توانند ماه‌ها نسخه قدیمی را نشان بدهند. استفاده امن یعنی همیشه هش در نام فایل + Cache-Control: max-age=31536000, immutable.

ترکیب درست این چهار قطعه (Cache-Control، stale-while-revalidate، ETag و ابطال کش تگ‌محور) همان چیزی است که تفاوت میان یک سایت متوسط و یک سایت واقعاً سریع را می‌سازد. در پروژه‌های خودم این استراتژی، کنار یک لایه edge سالم، کاری کرده که ۹۵ درصد درخواست‌ها بدون درگیر کردن Origin پاسخ داده شوند. اگر همین امروز فقط immutable را روی دارایی‌های هش‌دار و stale-while-revalidate را روی endpointهای پویا اضافه کنید، همین شب TTFB سایت‌تان پایین می‌آید.

Mateo Silva
درباره نویسنده Mateo Silva

Full-stack performance lead bridging frontend perf with backend latency. Cache invalidation is his love language.