🚀 امروز Workers Cache را معرفی می‌کنیم: یک کش چندلایه که جلوتر از Worker شما قرار می‌گیرد، با یک خط پیکربندی در Wrangler و همان هدرهای آشنا مثل Cache-Control. وقتی Workers Cache فعال باشد، هر درخواست کش‌پذیر ابتدا به cache روی Cloudflare می‌خورد؛ اگر پاسخ تازه‌ای موجود باشد، Cloudflare همان‌را برمی‌گرداند — Worker شما اجرا نمی‌شود و هزینه CPU پرداخت نمی‌کنید. در صورت miss، Worker اجرا می‌شود و اگر پاسخ کش‌پذیر باشد، Cloudflare آن را ذخیره می‌کند تا درخواست بعدی از هر جای دنیا مستقیماً از کش سرو شود.

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": {
    "enabled": true
  }
}

پس از این پیکربندی، کنترل کش را دقیقاً به همان روشی که HTTP طراحی کرده است انجام می‌دهید — با تنظیم هدرها روی پاسخ‌ها:

return new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
    "Cache-Tag": "products,product:123",
  },
});

وقتی محتوا تغییر می‌کند، Worker خودش می‌تواند کش را پاک کند:

await ctx.cache.purge({ tags: ["product:123"] });

این API تمام چیزی است که نیاز دارید. نیازی به تنظیم zone جدا، rules engine پیچیده، پروویژن کردن کش جداگانه یا ورود به محصول دوم نیست. کد Worker خودش سطح پیکربندی است و کش همراه همان Worker در هر جایی که اجرا شود دنبال آن می‌آید — روی دامنهٔ سفارشی، workers.dev، پشت service binding، در preview یا در یک Workers for Platforms tenant. یک Worker، یک کش، یک بار پیکربندی.

در سطح کاربری، امکانات زیادی زیرِ این سطح قرار دارد: کش tiered در تمام شبکه، پشتیبانی کامل از stale-while-revalidate تا پاسخ‌های قدیمی مانع تجربهٔ کاربر نشوند، content negotiation با Vary، کلیدهای کش multi-tenant-safe با ctx.props، پاک‌سازی برنامه‌ای بر اساس tag یا پیشوند مسیر، و نکته‌ای که برای ما بیشترین اهمیت را دارد — کشی که جلوتر از هر entrypoint از Worker قرار می‌گیرد، نه فقط entrypoint عمومی، با کنترل per-entrypoint برای فعال یا غیرفعال کردن کش. این یعنی می‌توانید کش را مستقیم داخل ساختار اپ خود جاسازی کنید: یک زنجیره از entrypointها که مرحله‌های کش هر جا خواستید داخل آن قرار می‌گیرند و با کد دو طرف پیکربندی می‌شوند. در ادامه همهٔ این موارد را مرور می‌کنیم.

Workers Cache هم‌اکنون برای همهٔ Workerها در هر پلانی در دسترس است و از طریق Wrangler فعال می‌شود. این همان API کش است که همیشه برای Workers می‌خواستیم. در ادامه می‌گوییم چرا زمان زیادی طول کشید تا به اینجا برسیم، چه چیزهایی الآن ممکن می‌شوند و چه چیزهایی در راه است.

چرا اپ‌های server-rendered به یک کش در جلو نیاز دارند

وقتی Workers را در 2017 معرفی کردیم، مدل این بود که می‌توانید کد را در شبکهٔ Cloudflare اجرا کنید تا درخواست‌ها را قبل از رسیدن به origin دستکاری کنید. Worker جلوتر از کش و origin قرار داشت و برای استفاده‌هایی مثل اضافه کردن هدر، بازنویسی URL، A/B testing یا فیلتر ترافیک قبل از origin مناسب بود. این مدل به کاربران کنترل کامل می‌داد که چه چیزی کش شود و چه چیزی نه، و خیلی‌ها با آن چیزهای جالبی ساختند.

اما دنیا تغییر کرد. Worker دیگر فقط یک لایه روی origin نبود، خودش تبدیل به origin شد. فریم‌ورک‌هایی مثل Astro، TanStack Start، Next.js، Remix و SvelteKit adapterهای Cloudflare دارند که اپ را به شکل Worker می‌سازند. دیگر origin جدا وجود ندارد — Worker سرور است.

وقتی Worker خودش origin است، معماری قبلی چیزی برای کش کردن ندارد. هر درخواست کد شما را اجرا می‌کند، حتی وقتی پاسخ دقیقاً همان bytes ای باشد که یک ثانیه قبل برگردانده بودید. runtime های Worker آن‌قدر سریع هستند که این کار ممکن است، اما «به اندازهٔ کافی سریع برای رندر هر درخواست» همچنان هزینهٔ latency و مصرف CPU در هر Invocation را دارد. در اپ‌های server-rendered، هر بار بارگذاری صفحه یعنی یک رندر.

Workers Cache معماری را برمی‌گرداند: حالا کش Cloudflare جلوتر از Worker قرار می‌گیرد. روی cache hit، Worker اصلاً اجرا نمی‌شود. Cloudflare پاسخ کش شده را برمی‌گرداند و صورتحساب CPU شما صفر می‌ماند. روی miss، Worker یک‌بار اجرا می‌شود، کش را پر می‌کند و درخواست بعدی — از هر جای دنیا — از کش سرو می‌شود بدون اجرای کد شما.

این همان چیزی بود که برای server-side rendering روی Workers کم داشتیم. قبلاً باید بین دو گزینهٔ ناامیدکننده انتخاب می‌کردید:

– همه چیز را هنگام ساخت prerender کنید (SSG). سریع برای کاربر اما هر تغییر نیاز به rebuild و redeploy دارد. برای سایت مستندات چند هزار صفحه‌ای چند دقیقه طول می‌کشد؛ برای یک فروشگاه بزرگ بدتر.

– یا هر صفحه را در هر درخواست رندر کنید. محتوای همیشه به‌روز، اما هر بار هزینهٔ رندر و latency را پرداخت می‌کنید.

Workers Cache گزینهٔ سومی می‌دهد: سرور-رندر on demand، پاسخ رندر شده را کش کنید، و آن را بر اساس TTL که شما تعیین می‌کنید تازه‌سازی کنید. اولین درخواست برای یک صفحهٔ جدید هنوز رندر می‌شود. هر درخواست بعدی تا زمان انقضای کش مثل صفحهٔ استاتیک سرو می‌شود. وقتی کش منقضی شد، درخواست بعدی رندر دوباره را فعال می‌کند — و با stale-while-revalidate حتی آن درخواست هم منتظر نمی‌ماند.

شما سرعت یک سایت استاتیک بدون زمان build و تازگی سرور-رندر را بدون هزینهٔ مداوم دارید. نیاز به مکانیزم‌های framework-specific مثل Incremental Static Regeneration نیست — فقط HTTP caching که همان‌طور که طراحی شده عمل می‌کند، جلوتر از کدی که به عنوان origin طراحی شده است.

stale-while-revalidate همان بخشی است که حس فوری بودن را فراهم می‌کند

دستورالعمل stale-while-revalidate به Cloudflare اجازه می‌دهد وقتی یک پاسخ کش منقضی شد، نسخهٔ قدیمی را فوراً سرو کند و هم‌زمان در پس‌زمینه آن را تازه کند. Cloudflare پشتیبانی کامل از stale-while-revalidate را اوایل امسال عرضه کرد و این همان چیزی‌ست که «ما Worker شما را کش می‌کنیم» را تبدیل می‌کند به «سایتِ Worker شما مثل استاتیک حس می‌شود.»

بدون این ویژگی، اولین درخواست بعد از انقضای کش باید صبر کند تا Worker صفحه را از نو رندر کند؛ کاربر آن latency را می‌بیند. با stale-while-revalidate، اولین درخواست بعد از انقضا صفحهٔ stale را بلافاصله دریافت می‌کند (با هدر Cf-Cache-Status: UPDATING) و Worker در پس‌زمینه کش را refill می‌کند. هیچ کاربری منتظر نمی‌ماند؛ حتی کسی که refresh را فعال کرده، پاسخ با سرعت کش دریافت می‌کند.

export default {
  async fetch(request) {
    const html = await renderPage(request);
    return new Response(html, {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        // Treat as fresh for 5 minutes; serve stale for up to an hour
        // while a background refresh runs.
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
      },
    });
  },
};

مدل ذهنی که باعث می‌شود این موضوع قابل هضم شود:

– Fresh window (max-age): Cloudflare پاسخ کش شده را می‌دهد. Worker اجرا نمی‌شود. ✅

– Stale window (stale-while-revalidate): Cloudflare نسخهٔ کش شده را می‌دهد و Worker در پس‌زمینه برای تجدید آن اجرا می‌شود. هیچ کاربری منتظر نمی‌ماند. ✅

– Outside both windows: Cloudflare Worker را اجرا می‌کند تا یک پاسخ تازه تولید کند، و کاربر باید برای آن رندر صبر کند. ⏳

شما بازه‌ها را انتخاب می‌کنید. برای کاتالوگ محصول که هر چند دقیقه آپدیت می‌شود، max-age=300 و stale-while-revalidate=3600 یعنی بازدیدکننده‌ها عملاً هرگز منتظر نمی‌مانند و Worker به اندازهٔ کافی اجرا می‌شود تا محتوا تازه بماند. برای آرشیو بلاگ که به ندرت تغییر می‌کند، max-age=86400 و stale-while-revalidate=2592000 یعنی Worker روزی یک‌بار به ازای هر صفحه اجرا می‌شود.

اولین درخواست به یک URL کاملاً جدید تنها موردی است که هزینهٔ رندر کامل را می‌پردازد. بعد از آن، صفحه برای بازدیدکننده‌ها مانند خروجی استاتیک رفتار می‌کند، در حالی که Worker همچنان مسئول تولید آن است.

یک URL، چندین نمایه: Vary چگونه کار می‌کند

در اپ‌های واقعی به ندرت یک URL به همهٔ کلاینت‌ها همان bytes را برمی‌گرداند. همان صفحهٔ محصول ممکن است برای مرورگر HTML و برای یک API client JSON برگرداند. یک تصویر ممکن است WebP برای کلاینت‌هایی که پشتیبانی می‌کنند و JPEG برای بقیه. صفحهٔ خانه ممکن است بسته به زبان کاربر به انگلیسی، فرانسوی یا ژاپنی برگردد.

بدون کش، انجام این کار ساده است — Worker هدرهای درخواست را می‌خواند و جواب مناسب را برمی‌گرداند. اما وقتی کش بین باشد، اوضاع معمولاً زشت می‌شود: بیشتر کش‌ها دو گزینهٔ بد می‌دهند — یا برای URLهایی با چند نمایه چیزی را کش نکنید، یا فقط یک نمایه را کش کنید و آن را به همه تحویل دهید.

Workers Cache از هدر استاندارد HTTP Vary پشتیبانی می‌کند که روش درست حل این مسئله است. وقتی Worker پاسخی با Vary: Accept-Encoding (یا Accept، یا Accept-Language، یا هر هدر دیگری) برمی‌گرداند، Cloudflare برای هر ترکیب متمایز از آن هدرها یک variant جداگانه ذخیره می‌کند — و فقط variant ای را برمی‌گرداند که مقادیر ذخیره‌شده‌اش با درخواست ورودی مطابقت داشته باشد.

export default {
  async fetch(request) {
    const accept = request.headers.get("Accept") ?? "";
    const wantsWebp = accept.includes("image/webp");
    const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();
    return new Response(body, {
      headers: {
        "Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
        "Cache-Control": "public, max-age=3600",
        // Cache a separate variant per distinct Accept header value.
        Vary: "Accept",
      },
    });
  },
};

یک URL، دو variant کش شده. مرورگری که هدر Accept: image/webp,*/* می‌فرستد WebP را می‌گیرد؛ مرورگری که Accept: image/jpeg می‌فرستد JPEG را می‌گیرد. هر دو از کش سرو می‌شوند. Worker هر دو variant را هنگام اولین درخواست برای هر کدام می‌نویسد، و بعد از آن هیچ‌گاه برای آن‌ها اجرا نمی‌شود.

این روش استاندارد HTTP برای content negotiation است و Workers Cache آن را مطابق RFC 9110 و RFC 9111 پیاده‌سازی می‌کند. هیچ فهرست مجازی از هدرهایی که می‌توانید Vary کنید وجود ندارد — هر هدرِ لازم را لیست می‌کنید و Cloudflare بر اساس مقادیر دقیق آن‌ها variant می‌سازد. مستندات توضیح داده‌اند که چگونه fan-out سربارهٔ variant را با نرمال‌سازی هدرها در یک gateway Worker کنترل کنید، چرا purgeها همهٔ variantهای یک URL را با هم invalid می‌کنند و تنها حالتی که caching را کاملاً غیرفعال می‌کند Vary: * است.

این کش، متعلق به Worker شماست، نه zone شما — ادامه در بخش بعدی…