تنظیم Cache Headers در وردپرس یعنی تعیین دقیق مدت ماندگاری و شرایط اعتبار هر پاسخ، به‌گونه‌ای که مرورگر و لایه‌های میانی بدانند چه چیزی را نگه دارند و چه چیزی را نگیرند.

هدرهای کش (Cache Headers) تصمیم کش را از سمت سرور به سمت مرورگر منتقل می‌کنند و همین جابه‌جایی، تفاوت میان تجربه سریع و تجربه تکراری را می‌سازد.

سه دسته اصلی وجود دارد: کش طولانی‌مدت برای فایل‌های نسخه‌دار، کش کوتاه‌مدت برای HTML و ممنوعیت کامل برای داده شخصی.

خطا در هر یک از این سه، یا محتوای قدیمی را نشان می‌دهد یا ظرفیت سرور را با درخواست‌های تکراری هدر می‌دهد.

هدف این نوشته، رسیدن از تنظیمات پراکنده به یک سیاست کش منسجم و قابل دفاع در محیط تولید است.

پرتکرارترین صحنه‌ای که با آن روبه‌رو شده‌ام این است: کسی فایل CSS را ویرایش می‌کند، سایت را دوباره باز می‌کند و تغییری نمی‌بیند. به‌جای بررسی هدرهای کش، سراغ افزونه کش و مرورگر می‌رود و مدتی را در مسیر اشتباه طی می‌کند. ریشه، در همان هدر کوچکی است که کسی وقت نکرده درباره‌اش تصمیم بگیرد.

Cache Header چیست و چه کسی تصمیم می‌گیرد

هر پاسخ HTTP مجموعه‌ای از هدرها را حمل می‌کند که فراداده‌ای درباره محتوای پاسخ هستند. بخشی از این هدرها مستقیماً با کش مرتبط‌اند و مشخص می‌کنند که پاسخ چقدر معتبر است، آیا قابل کش است و در چه شرایطی باید اعتبارسنجی مجدد شود.

تصمیم کش در سه نقطه گرفته می‌شود. نخست، سرور مبدأ (Origin Server) که پاسخ را می‌سازد. دوم، لایه‌های میانی مانند پروکسی و شبکه توزیع محتوا (Content Delivery Network یا CDN) که میان سرور و کاربر قرار می‌گیرند. سوم، مرورگر کاربر که در نهایت تصمیم می‌گیرد آیا نسخه کش‌شده را نمایش دهد یا درخواست تازه بفرستد.

هدرها (Web Cache) زبان مشترک این سه لایه‌اند. اگر این زبان روشن نباشد، هر لایه تصمیم خودش را می‌گیرد و نتیجه، رفتاری غیرقابل پیش‌بینی می‌شود که عیب‌یابی آن زمان زیادی می‌برد.

در وردپرس، سرور مبدأ هم کد PHP است و هم لایه وب‌سرور. یعنی محل تولید هدرها می‌تواند از یک افزونه تا فایل تنظیمات وب‌سرور متغیر باشد. همین پراکندگی، منبع اصلی ناهماهنگی در سیاست کش است. پیش از هر تنظیمی باید تصویر روشنی از بهینه‌سازی سرعت سایت وجود داشته باشد تا هدرها در همان چارچوب دیده شوند.

سه لایه کش: مرورگر، پروکسی میانی، CDN

لایه مرورگر نزدیک‌ترین لایه به کاربر است و کم‌هزینه‌ترین کش به‌شمار می‌رود. وقتی مرورگر یک پاسخ را در حافظه یا روی دیسک نگه می‌دارد، درخواست بعدی به سرور ارسال نمی‌شود. این لایه، هزینه انتقال را حذف می‌کند و بخش بزرگی از اثر Cache Header از همین‌جا می‌آید.

لایه میانی، میان سرور و کاربر قرار می‌گیرد. این لایه می‌تواند یک پروکسی سازمانی، یک پروکسی ISP یا یک لایه شبکه توزیع محتوا باشد. تصمیم کش در این لایه بر پایه همان هدرها گرفته می‌شود، اما گاهی سیاست‌های محلی نیز بر آن اثر می‌گذارند.

لایه شبکه توزیع محتوا، گسترده‌ترین لایه است. نسخه‌هایی از پاسخ روی گره‌های لبه (Edge Node) نگه داشته می‌شود و کاربر از نزدیک‌ترین گره سرو می‌شود. نحوه کار نقش CDN در بهبود سرعت سایت مستقیماً به درستی هدرها وابسته است؛ اگر هدر نگوید پاسخ قابل کش است، CDN هم آن را کش نخواهد کرد.

نکته کلیدی این است که این سه لایه یک زنجیره می‌سازند. اگر یکی از حلقه‌ها سیاست متفاوتی داشته باشد، نتیجه نهایی ناهماهنگ می‌شود. در پروژه‌هایی که فقط بخشی از زنجیره تنظیم شده بوده، اغلب شاهد رفتارهای عجیبی مانند کش شدن صفحه سبد خرید یا کش نشدن تصاویر بوده‌ام.

هدر کش، توصیه‌ای به لایه‌های میانی است؛ اما گاهی همین توصیه، تنها چیزی است که میان یک سایت سریع و یک سایت کند تفاوت می‌سازد.

خانواده هدرهای اصلی و نقش هر یک

هدرهای کش در چند خانواده دسته‌بندی می‌شوند. شناخت این خانواده‌ها به تصمیم‌گیری دقیق‌تر کمک می‌کند.

خانواده اول، هدرهای سقف زمانی هستند: Cache-Control با پارامتر max-age و هدر قدیمی Expires. این دو مشخص می‌کنند پاسخ تا چه زمانی تازه (Fresh) به‌شمار می‌آید.

خانواده دوم، هدرهای کنترل‌کننده دامنه کش هستند: public، private، no-cache و no-store. این چهار مشخص می‌کنند چه کسی مجاز است پاسخ را کش کند.

خانواده سوم، هدرهای اعتبارسنجی هستند: ETag و Last-Modified. این دو به مرورگر اجازه می‌دهند درخواست شرطی بفرستد و پاسخ کوچک 304 Not Modified دریافت کند، به‌جای انتقال مجدد کل بدنه.

خانواده چهارم، هدرهای رفتار جانبی مانند Vary و Age هستند. هدر Vary تعیین می‌کند کش باید بر اساس چه چیزی کلید خود را بسازد؛ مثلاً نسخه موبایل و دسکتاپ جدا کش شوند. هدر Age نشان می‌دهد پاسخ چه مدت در لایه میانی مانده است.

هدرنقش اصلیمقدار رایج
Cache-Controlتعیین سقف و دامنه کشpublic, max-age=31536000, immutable
Expiresسقف زمانی مطلق (قدیمی)تاریخ مشخص
ETagشناسه نسخه پاسخهش محتوا
Last-Modifiedآخرین زمان تغییرتاریخ
Varyکلید تفکیک کشAccept-Encoding

HTML و مسئله محتوای پویا

فایل‌های HTML وردپرس ماهیت پویا دارند. یک صفحه می‌تواند برای کاربران مختلف متفاوت باشد، بخش‌های شخصی‌سازی‌شده داشته باشد یا بر اساس ورود کاربر تغییر کند. به همین دلیل، تنظیم سقف زمانی طولانی روی HTML خطرناک است.

رویکرد متعادل، ترکیب دو استراتژی است. سقف زمانی کوتاه برای HTML که امکان به‌روزرسانی سریع را حفظ کند، و استفاده از مکانیزم‌های دیگر برای تازه‌سازی بی‌درنگ محتوا. در سایت‌هایی که افزونه کش دارند، این لایه معمولاً بخش بزرگی از تصمیم را بر عهده می‌گیرد.

انتخاب و تنظیم این افزونه‌ها موضوعی است که در بهترین افزونه‌های کش وردپرس به‌تفصیل بررسی شده است. نکته مهم این است که هدر HTML نباید سقف طولانی داشته باشد، حتی وقتی خود محتوا از کش افزونه سرو می‌شود.

در سایت‌هایی که از شبکه توزیع محتوا استفاده می‌کنند، سیاست کش HTML باید با سیاست CDN هم‌خوان باشد. اگر CDN نسخه‌ای را برای مدت طولانی نگه دارد و هدر بگوید کوتاه، در عمل رفتار نهایی مبهم می‌شود. تنظیم درست راه‌اندازی CDN برای وردپرس بدون هم‌خوانی هدرها، عملاً هزینه‌ای است که بازدهی محدودی می‌دهد.

فایل‌های نسخه‌دار و Cache Busting

فایل‌های استاتیک مانند CSS، JavaScript، تصاویر و فونت‌ها وقتی تغییر نمی‌کنند، بهترین کاندید برای کش طولانی‌مدت هستند. مسئله این است که وقتی تغییر می‌کنند، چگونه مرورگر را از کش قدیمی خارج کنیم.

راه‌حل استاندارد، نسخه‌گذاری در نام فایل یا در پارامتر آدرس است. وقتی نام فایل عوض می‌شود، از نظر مرورگر یک منبع کاملاً جدید است و درخواست تازه صادر می‌شود. به این کار Cache Busting گفته می‌شود.

# Nginx - static assets with hashed filenames
location ~* .(css|js|woff2|webp|avif|svg)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

پارامتر immutable به مرورگر می‌گوید که در طول سقف زمانی، حتی درخواست شرطی هم نفرستد. این پارامتر تنها وقتی امن است که نام فایل واقعاً نسخه‌دار باشد، در غیر این صورت کاربر نسخه قدیمی را تا انقضای کش می‌بیند.

در وردپرس، بسیاری از قالب‌ها و افزونه‌ها پارامتر نسخه را به انتهای آدرس فایل‌ها اضافه می‌کنند. این روش ساده است، اما در همه لایه‌های میانی به‌درستی مدیریت نمی‌شود. نسخه‌گذاری در نام فایل، مطمئن‌ترین روش است.

توجه به تصاویر در این میان اهمیت ویژه‌ای دارد، چون بخش بزرگی از حجم صفحات را تشکیل می‌دهند. تنظیم نادرست کش تصاویر می‌تواند اثر منفی تأثیر تصاویر سنگین بر Core Web Vitals را چند برابر کند، چون هر بار بازخوانی می‌شوند.

منابع شخصی و سیاست no-store

بخشی از پاسخ‌های وردپرس ماهیت شخصی دارند: صفحه پیشخوان، سبد خرید، تسویه حساب، پروفایل کاربر. این پاسخ‌ها هرگز نباید در کش مرورگر یا لایه‌های میانی بمانند، چون می‌توانند به کاربر دیگری نشان داده شوند.

دو هدر برای این کار وجود دارد: no-cache و no-store. تفاوت این دو مهم است. no-cache می‌گوید پاسخ را کش نکن، اما هر بار اعتبارسنجی کن. no-store می‌گوید اصلاً چیزی ذخیره نکن.

برای داده‌های حساس، no-store انتخاب درست است. برای پاسخ‌های پویا که امکان اعتبارسنجی دارند، no-cache به‌همراه ETag کارآمدتر است، چون اگر محتوا تغییر نکرده باشد، پاسخ کوچک 304 کافی است.

در تنظیم این سیاست، باید مراقب اثر پروانه‌ای بود. اگر یک افزونه هدر no-store را روی همه پاسخ‌ها اعمال کند، کل مزیت کش از بین می‌رود. مشابه همین خطا در پیکربندی افزونه کش وردپرس پرتکرار است و باید به‌عنوان یک الگوی هشداردهنده در نظر گرفته شود.

هر هدر که برای امنیت اعمال می‌شود، اگر بیش از حد گسترده باشد، به یک هزینه عملکردی دائمی تبدیل می‌شود.

تنظیم هدرها در Apache و Nginx

لایه وب‌سرور نزدیک‌ترین نقطه به تولید پاسخ است و تنظیم هدرها در این لایه، پایدارترین گزینه به‌شمار می‌رود. مستقل از کد وردپرس، این لایه می‌تواند سیاست کش را برای دسته‌های مشخصی از آدرس‌ها اعمال کند.

در Apache، ماژول mod_expires و ماژول mod_headers ابزارهای اصلی هستند. در فایل .htaccess یا فایل تنظیمات اصلی، این هدرها اعمال می‌شوند. انتخاب میان این دو محل، خودش موضوعی است که در بهینه‌سازی Apache برای وردپرس به‌تفصیل آمده است؛ به‌طور خلاصه، انتقال قواعد پایدار به فایل تنظیمات اصلی، هزینه خواندن .htaccess در هر درخواست را حذف می‌کند.

<IfModule mod_headers.c>
    <FilesMatch ".(css|js|webp|avif|woff2)$">
        Header set Cache-Control "public, max-age=31536000, immutable"
    </FilesMatch>
    <FilesMatch ".(html|php)$">
        Header set Cache-Control "private, no-cache, must-revalidate"
    </FilesMatch>
</IfModule>

در Nginx، بلوک location و دستور add_header ابزارهای اصلی‌اند. توجه به این نکته ضروری است که add_header در Nginx فقط برای کدهای پاسخ مشخصی اعمال می‌شود؛ اگر این نکته نادیده گرفته شود، ممکن است هدرها روی پاسخ‌های خطا اعمال نشوند. این رفتار در بهینه‌سازی Nginx برای وردپرس به‌عنوان یکی از دام‌های رایج پیکربندی بررسی شده است.

در هر دو حالت، ترتیب قواعد اهمیت دارد. اگر قاعده‌ای برای همه فایل‌ها هدر تعیین کند و قاعده‌ای دقیق‌تر پس از آن بیاید، ممکن است نادیده گرفته شود. آگاهی از این ترتیب، از خطاهایی که بعداً ظاهر می‌شوند جلوگیری می‌کند.

تنظیم هدرها در لایه وردپرس

بخشی از هدرها را نمی‌توان در لایه وب‌سرور تعیین کرد، چون به وضعیت کاربر یا محتوای صفحه بستگی دارند. در این موارد، لایه وردپرس باید تصمیم بگیرد و هدر مناسب را ارسال کند.

هوک send_headers نقطه ورود استاندارد است. این هوک پیش از ارسال هدرهای اصلی اجرا می‌شود و امکان تنظیم دقیق آن‌ها را می‌دهد.

add_action( "send_headers", function () {
    if ( is_user_logged_in() || is_admin() ) {
        header( "Cache-Control: private, no-cache, must-revalidate" );
        return;
    }
    if ( is_front_page() || is_home() ) {
        header( "Cache-Control: public, max-age=600, stale-while-revalidate=86400" );
    }
} );

نکته مهم این است که هر هدر تنظیم‌شده در لایه وردپرس باید با سیاست لایه وب‌سرور هم‌خوان باشد. اگر وب‌سرور هدر HTML را روی یک سال تنظیم کند و وردپرس پس از آن هدر کوتاه‌تری اضافه کند، نتیجه مبهم و وابسته به پیاده‌سازی لایه‌های میانی می‌شود.

در محیط‌هایی که چند سایت روی یک سرور اجرا می‌شوند، تنظیمات کش باید در سطح هر سایت جداگانه اعمال شود. همان اصلی که در تنظیم PHP-FPM برای وردپرس درباره جداسازی استخرها بر آن تأکید شد، در سیاست کش نیز برقرار است.

Stale-While-Revalidate و الگوهای مدرن

الگوی Stale-While-Revalidate امکان می‌دهد پاسخ منقضی‌شده، برای مدتی مشخص همچنان سرو شود در حالی که نسخه تازه در پس‌زمینه ساخته می‌شود. نتیجه، تجربه‌ای است که در آن کاربر هیچ‌گاه با انتظار روبه‌رو نمی‌شود، حتی هنگام انقضای کش.

Cache-Control: public, max-age=60, stale-while-revalidate=86400

تأثیر این الگو روی معیار زمان پاسخ اول کاملاً مشهود است، چون درخواست کاربر با یک پاسخ آماده روبرو می‌شود. همین ویژگی، آن را به یکی از مؤثرترین ابزارها برای بهبود کاهش زمان TTFB تبدیل می‌کند.

برای محتوایی که به‌سرعت تغییر می‌کند، پارامتر stale-if-error نیز مفید است. اگر سرور مبدأ در زمان اعتبارسنجی خطا بدهد، لایه کش می‌تواند پاسخ قدیمی را سرو کند و به این ترتیب، تجربه کاربر حتی در شرایط خطای موقت پایدار می‌ماند.

این الگوها در پروژه‌های فروشگاهی اهمیت دوچندان دارند. هر ثانیه تأخیر در نمایش صفحه محصول، می‌تواند مستقیماً روی نرخ تبدیل اثر بگذارد. مطالعات در این حوزه در تأثیر سرعت سایت بر نرخ تبدیل به‌تفصیل بررسی شده است.

سنجش و اعتبارسنجی سیاست کش

هر سیاست کش باید پس از اعمال، سنجیده و تأیید شود. بدون این مرحله، تفاوت میان تنظیم درست و تنظیم ظاهراً درست قابل تشخیص نیست.

ابزار نخست، تب Network در ابزار توسعه‌دهنده مرورگر است. در این تب می‌توان دید هر درخواست از کش سرو شده یا از سرور. ستون Size نشان می‌دهد داده از حافظه محلی آمده یا از شبکه.

ابزار دوم، خط فرمان است. با ابزارهایی مانند curl می‌توان هدرهای پاسخ را دقیق مشاهده و با چند بار درخواست، رفتار کش را سنجید.

curl -I https://example.com/wp-content/themes/mytheme/style.css
curl -I https://example.com/

خروجی این دستور، هدرهای واقعی ارسالی سرور را نشان می‌دهد. مقایسه این خروجی با آنچه انتظار می‌رود، منبع اصلی اعتبارسنجی است. این کار روی سطوح مختلف سایت و انواع مختلف منابع باید انجام شود تا تصویر کامل به‌دست آید.

در سطح تجربه کاربر، معیارهای کیفیت باید اندازه‌گیری شوند. بهبود TTFB و LCP نشانه‌های مستقیم اثرگذاری سیاست کش هستند. تیم‌هایی که این معیارها را جدی می‌گیرند، معمولاً از ابزارهایی استفاده می‌کنند که در ابزارهای تست سرعت سایت معرفی شده‌اند و اثر تغییرات را به‌طور مداوم رصد می‌کنند.

سیاست کش بدون اندازه‌گیری، یک فرض است؛ و فرض‌ها در محیط تولید هزینه دارند.

جدول تصمیم‌گیری سیاست کش

نوع منبعCache-Control پیشنهادیدلیل
CSS و JS نسخه‌دارpublic, max-age=31536000, immutableنام فایل در هر تغییر عوض می‌شود
تصاویر و فونت‌هاpublic, max-age=31536000نادر تغییر می‌کنند
HTML صفحه اصلیpublic, max-age=60, stale-while-revalidate=86400پویا اما با امکان سرو نسخه قدیمی
پیشخوان و سبد خریدprivate, no-cache, must-revalidateداده شخصی، نیازمند اعتبارسنجی هر بار
داده حساس و توکنno-storeممنوعیت کامل ذخیره‌سازی

اشتباهات رایج

نخستین اشتباه، اعمال سقف طولانی روی HTML است. نتیجه، نمایش محتوای قدیمی به کاربر و سردرگمی در به‌روزرسانی سایت. این خطا در سایت‌های خبری و فروشگاهی بیشترین آسیب را می‌زند.

دومین اشتباه، استفاده از no-store به‌جای no-cache برای پاسخ‌های پویا است. نتیجه، از دست رفتن امکان اعتبارسنجی شرطی و افزایش بار سرور.

سومین اشتباه، نادیده گرفتن هدر Vary است. اگر سایت نسخه متفاوتی برای موبایل و دسکتاپ سرو کند و این تفکیک در هدر Vary لحاظ نشود، ممکن است کاربر موبایل نسخه دسکتاپ را ببیند یا برعکس.

چهارمین اشتباه، تنظیم هدرها در لایه‌های مختلف بدون هم‌خوانی است. اگر وب‌سرور، وردپرس و CDN هر یک سیاست متفاوتی اعمال کنند، رفتار نهایی غیرقابل پیش‌بینی می‌شود.

پنجمین اشتباه، فراموش‌کردن فایل‌های نسخه‌دار است. اگر نام فایل ثابت بماند و هدر immutable اعمال شود، کاربر تا یک سال نسخه قدیمی را می‌بیند؛ حالتی که رفع آن نیازمند تغییر نام فایل یا پاک‌سازی دستی کش در همه مرورگرها است.

ششمین اشتباه، بی‌توجهی به تصاویر است. تنظیم نادرست کش روی تصاویر، بار سرور را بی‌دلیل بالا می‌برد. این موضوع با مسیر مقایسه WebP و JPEG برای سرعت هم‌راستا است، چون انتخاب فرمت و سیاست کش، دو روی یک سکه هستند.

پرسش‌های پرتکرار درباره هدرهای کش در وردپرس

تفاوت no-cache و no-store چیست؟

no-cache یعنی پاسخ را ذخیره نکن اما هر بار اعتبارسنجی کن؛ اگر محتوا تغییر نکرده باشد، پاسخ ۳۰۴ کوچک کافی است. no-store یعنی هرگز چیزی ذخیره نکن، حتی برای اعتبارسنجی. برای داده حساس، no-store انتخاب درست است.

چرا تغییرات CSS روی سایت اعمال نمی‌شود؟

اگر فایل CSS با سقف زمانی طولانی کش شده و نام آن تغییر نکرده باشد، مرورگر نسخه قدیمی را نمایش می‌دهد. راه‌حل استاندارد، نسخه‌گذاری در نام فایل یا پارامتر آدرس است تا مرورگر آن را منبع جدید بداند.

هدر Expires بهتر است یا Cache-Control؟

در محیط‌های مدرن، Cache-Control انتخاب اول است. هدر Expires برای سازگاری با کلاینت‌های قدیمی نگه داشته می‌شود و در کنار Cache-Control قرار می‌گیرد، نه به‌جای آن.

آیا باید Cache-Control را در لایه وردپرس تنظیم کرد یا وب‌سرور؟

سیاست‌های پایدار و عمومی برای فایل‌های استاتیک بهتر است در لایه وب‌سرور تنظیم شوند، چون سریع‌تر و مستقل از کد اجرا می‌شوند. تصمیم‌های وابسته به وضعیت کاربر یا محتوا باید در لایه وردپرس با هوک send_headers گرفته شوند.

هدر Vary چه زمانی ضروری است؟

وقتی پاسخ بر اساس یک هدر درخواست متفاوت می‌شود. مثلاً اگر سایت نسخه فشرده‌شده و فشرده‌نشده سرو می‌کند، Vary: Accept-Encoding ضروری است. بدون این هدر، ممکن است پاسخ نامناسب به کاربر ارسال شود.

آیا Cache Header روی سئو اثر دارد؟

اثر مستقیم ندارد، اما اثر غیرمستقیم آن جدی است. زمان پاسخ پایین‌تر و تجربه کاربری روان‌تر، معیارهای Core Web Vitals را بهبود می‌دهد و این معیارها به‌عنوان سیگنال کیفیت در نظر گرفته می‌شوند.

آیا تنظیم هدرها روی همه فایل‌ها بی‌خطر است؟

خیر. تنظیم یکسان برای همه فایل‌ها، یکی از پرتکرارترین علت‌های نمایش محتوای قدیمی یا افشای داده شخصی است. سیاست کش باید بر اساس نوع منبع تفکیک شود.

یک نکته برای ادامه مسیر

سیاست کش، یک تصمیم واحد نیست؛ مجموعه‌ای از تصمیم‌های کوچک است که در سه لایه مختلف گرفته می‌شوند. هر تصمیم باید با تصمیم‌های دیگر هم‌خوان باشد و پس از اعمال، اندازه‌گیری شود. تفاوت میان یک سایت سریع و یک سایت پرتنش، اغلب در همین هم‌خوانی ظاهر می‌شود.

اگر روی پروژه خودتان این مسیر را طی کرده‌اید، خوشحال می‌شوم بدانم کدام بخش بیشترین زمان را گرفت: هم‌خوان‌کردن لایه وردپرس با وب‌سرور، انتخاب مقدار مناسب برای stale-while-revalidate، یا ردیابی هدرهایی که در لایه CDN بازنویسی می‌شدند. تجربه‌تان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حل متفاوتی پیدا کرده‌اید که می‌تواند برای خواننده بعدی مفید باشد.