یک استایل را عوض میکنید، فایل را آپلود میکنید، صفحه را رفرش میکنید — هیچ تغییری نیست. رفرش اجباری میزنید، باز هم نه. حالت ناشناس باز میکنید و آنجا درست است.
اگر این را تجربه کردهاید، احتمالاً با یک هدر خاص کش روبهرو شدهاید که خیلی از هاستها بهصورت پیشفرض میفرستند و رفتارش با کش معمولی فرق اساسی دارد.
هدر immutable چه میکند#
کش معمولی مرورگر اینطور کار میکند: فایل را نگه میدارد، و وقتی دوباره لازمش داشت یک درخواست شرطی میفرستد که یعنی «این نسخه را دارم، تغییر کرده؟». اگر سرور بگوید نه، پاسخ ۳۰۴ برمیگردد و فایل دوباره دانلود نمیشود. مهم این است که یک درخواست انجام شده و مرورگر فرصت داشته بفهمد فایل عوض شده.
حالا این هدر را ببینید:
Cache-Control: public, max-age=31536000, immutableکلمهی آخر معنایش این است: «تا پایان این مدت، این فایل قطعاً عوض نمیشود.» مرورگری که این را میبیند دیگر هیچ درخواستی نمیزند — نه شرطی، نه معمولی. فایل را مستقیم از دیسک میخواند.
نتیجه: حتی رفرش اجباری هم لزوماً دورش نمیزند. رفتار رفرش اجباری بین مرورگرها یکسان نیست و در بعضی حالتها فقط سند اصلی را تازه میکند نه همهی منابع را. ضمناً اگر کش واسطی هم وسط باشد، آن هم همان نسخه را نگه داشته.
اول مطمئن شوید همین است#
هدرهای واقعی را ببینید:
curl -sI https://example.com/assets/css/main.css | grep -i cache-controlاگر immutable در خروجی بود، مشکل پیدا شد.
در ابزار توسعهدهندهی مرورگر هم در زبانهی Network، ستون Size برای این فایل چیزی شبیه «disk cache» نشان میدهد و اصلاً درخواستی به سرور نمیرود. اگر تیک «Disable cache» را بزنید فایل تازه میآید و همین تأییدش میکند.
راهحل غلط و راهحل درست#
راهحل غلط: هدر را بردارید#
وسوسهانگیز است که در .htaccess هدر را خاموش کنید. کار میکند، ولی دارید یک بهینهسازی خوب را بهخاطر یک مشکل قابل حل قربانی میکنید. کش طولانی روی داراییهای ثابت یکی از مؤثرترین کارها برای سرعت است، مخصوصاً برای بازدیدکنندهی برگشتی.
راهحل درست: آدرس را عوض کنید#
اگر مرورگر قرار است فایل را برای همیشه کش کند، پس فایل جدید باید آدرس جدید داشته باشد. دو شکل دارد:
/assets/css/main.css?v=1712345678
/assets/css/main.a1b2c3.cssاولی سادهتر است. دومی برای شبکههای توزیع محتوا مطمئنتر است چون بعضی از آنها پارامتر پرسوجو را در کلید کش لحاظ نمیکنند.
پیادهسازی: نسخه از زمان تغییر فایل#
مقدار نسخه باید خودکار باشد. هر چیزی که دستی باشد بالاخره فراموش میشود.
سادهترین منبع خودکار، زمان آخرین تغییر خود فایل است:
class Asset
{
public static function url(string $path): string
{
$file = public_path($path);
if (! is_file($file)) {
return asset($path);
}
return asset($path) . '?v=' . filemtime($file);
}
}و در قالب:
<link rel="stylesheet" href="{{ \App\Support\Asset::url('assets/css/main.css') }}">حالا هر بار فایل عوض شود، مقدار v هم عوض میشود و مرورگر آن را یک منبع کاملاً جدید میبیند. بدون هیچ کار دستی.
تجربهی ما: بعد از ساختن این کلاس، هر جا از تابع پیشفرض
asset()استفاده شده بود را باید عوض میکردیم. یکی دو مورد جا ماند و دقیقاً همانها بودند که بعداً «بهروز نمیشوند». اگر چنین کمکی میسازید، همان موقع همهی مصرفکنندهها را جابهجا کنید.
نکتهی مسیر#
اگر پروژه بیرون از ریشهی وب است — که روی سیپنل ساختار معمول همین است — تابع public_path() لزوماً به جایی که فایل واقعاً سرو میشود اشاره نمیکند. مسیر واقعی را چک کنید:
$file = base_path('public/' . ltrim($path, '/'));چه چیزی را کش طولانی بدهیم و چه چیزی را نه#
قاعدهی ساده: هر چیزی که آدرسش نسخهدار است میتواند کش طولانی بگیرد. هر چیزی که آدرس ثابت دارد نباید.
| منبع | سیاست پیشنهادی |
|---|---|
| CSS و JS با نسخه در آدرس | یک سال، immutable |
| فونت | یک سال، immutable |
| تصویر آپلودی با نام یکتا | یک سال |
| صفحهی HTML | بدون کش یا کش خیلی کوتاه |
| فایل robots و sitemap | چند ساعت |
خطای رایج این است که کش طولانی روی خودِ صفحهی HTML گذاشته شود. آنوقت کاربر محتوای قدیمی میبیند و هیچ نسخهگذاریای نجاتش نمیدهد، چون آدرس صفحه که عوض نمیشود.
وقتی هنوز درست نشد#
اگر نسخهگذاری را اضافه کردید و باز هم قدیمی میبینید، به این ترتیب چک کنید:
۱. آیا HTML خروجی واقعاً نسخه دارد؟ منبع صفحه را ببینید، نه کد قالب را:
curl -s https://example.com | grep -o 'assets/css/[^"]*'اگر پارامتر نسخه در خروجی نیست، یعنی کش قالب لاراول هنوز نسخهی قدیمی را دارد:
php84 artisan view:clear
php84 artisan optimize:clear۲. آیا فایل روی سرور واقعاً عوض شده؟
ls -la --time-style=full-iso ~/app/public/assets/css/main.cssچند بار پیش آمده که فایل اصلاً آپلود نشده بود.
۳. آیا کش واسط دارید؟ اگر سایت پشت یک شبکهی توزیع محتوا است، باید کش آن را هم پاک کنید. سرآیند پاسخ معمولاً نشانهای دارد که بگوید پاسخ از کش آمده یا از سرور اصلی.
۴. آیا کش کد PHP فعال است؟ روی هاست اشتراکی گاهی کش کد سرور مقدار قدیمی فایل PHP را نگه میدارد. معمولاً پس از چند ثانیه خودش تازه میشود، ولی اگر بررسی زمان فایل خاموش باشد میتواند تا ریاستارت بماند. اگر تغییرات فایل PHP هم اعمال نمیشود، این را در نظر داشته باشید.
تنظیم هدرها در .htaccess#
اگر هاست شما این هدرها را نمیفرستد و میخواهید خودتان اضافهشان کنید، در .htaccess ریشهی وب:
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|woff2|png|jpe?g|webp|avif|svg)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>شرط IfModule را حذف نکنید. اگر ماژول روی سرور نباشد و شرط را برداشته باشید، کل سایت خطای ۵۰۰ میگیرد — و روی هاست اشتراکی که به لاگ وبسرور دسترسی راحت ندارید، پیدا کردن علتش وقت میبرد.
و یادتان باشد این هدر را فقط وقتی بگذارید که آدرسها نسخهدار باشند. ترتیب کار مهم است: اول نسخهگذاری، بعد کش طولانی. برعکسش یعنی چند ماه محتوای قدیمی برای بازدیدکنندههای قدیمی.
فشردهسازی را هم چک کنید#
در همین حوالی، یک چیز دیگر هم ارزش بررسی دارد. ببینید پاسخها فشرده میشوند یا نه:
curl -sI -H 'Accept-Encoding: gzip, br' https://example.com/assets/css/main.css | grep -i content-encodingاگر خالی بود، فایلهای متنی بدون فشردهسازی منتقل میشوند. برای یک فایل CSS چند ده کیلوبایتی، فشردهسازی معمولاً حجم انتقال را چند برابر کم میکند و روی اتصالهای کند تفاوتش محسوس است.
بودجهی دارایی: عددی که باید تعیین کنید#
یک تصمیم که زود گرفتنش بهتر است: برای هر دارایی یک سقف حجم بگذارید و پایبند بمانید. چیزی که ما داریم:
| دارایی | سقف |
|---|---|
| CSS فروشگاه | ۶۰ کیلوبایت |
| جاوااسکریپت فروشگاه | ۸ کیلوبایت |
| فونت فارسی | ۹۰ کیلوبایت |
| کتابخانهی خارجی | صفر |
بدون چنین سقفی، فایل CSS بهتدریج بزرگ میشود و هیچوقت لحظهای نیست که کسی بگوید «این زیادی بزرگ شده». با سقف، لحظهاش خودش میرسد.
وقتی به سقف نزدیک میشوید، راه درست تفکیک است نه فشردهسازی بیشتر: استایلی که فقط در صفحههای بلاگ لازم است میتواند به فایل جدا برود و فقط همانجا بارگذاری شود. همین کار را برای جاوااسکریپت هم بکنید — اسکریپت پنل مدیریت هیچ دلیلی ندارد که برای بازدیدکنندهی فروشگاه دانلود شود.
یک نکته دربارهی فونت#
فونت فارسی معمولاً بزرگترین دارایی ثابت صفحه است و باید حتماً خودمیزبان باشد. برای کاربر ایرانی، فونت از شبکههای توزیع محتوای خارجی یا خیلی کند میآید یا اصلاً نمیآید، و نتیجهاش چند ثانیه صفحهی بدون متن است.
خودمیزبان کنید، کش یکساله بدهید و در قواعد فونت font-display: swap بگذارید تا متن با فونت جایگزین نمایش داده شود تا وقتی فونت اصلی برسد.
کش لاراول را با کش مرورگر قاطی نکنید#
یک سردرگمی رایج: وقتی میگویند «کش را پاک کن»، منظور کدام کش است؟ روی یک پروژهی لاراول روی هاست اشتراکی، دستکم پنج لایهی جدا وجود دارد و هر کدام دستور خودش را دارد.
| لایه | چه چیزی را نگه میدارد | چطور پاک میشود |
|---|---|---|
| کش پیکربندی | مقدارهای فایل env و config | config:clear |
| کش مسیر | فهرست مسیرها | route:clear |
| کش قالب | فایلهای Blade کامپایلشده | view:clear |
| کش اپلیکیشن | هر چه خودتان ذخیره کردهاید | cache:clear |
| کش مرورگر و واسط | فایلهای ثابت | نسخهگذاری آدرس |
دستور optimize:clear چهار تای اول را با هم پاک میکند و همان چیزی است که معمولاً در اسکریپت استقرار میگذارید. ولی روی لایهی پنجم هیچ اثری ندارد — و دقیقاً همان لایهای است که این نوشته دربارهاش بود.
اگر بعد از استقرار چیزی تغییر نکرده، اول تشخیص دهید کدام لایه است:
- HTML خروجی عوض شده ولی استایل نه؟ لایهی پنجم. نسخهگذاری.
- HTML خروجی هم عوض نشده؟ لایهی سوم. کش قالب.
- مقدار تنظیمات قدیمی است؟ لایهی اول. کش پیکربندی.
این تشخیص ساده جلوی نصف وقت تلفشده را میگیرد.
استقرار بدون لحظهی ناهماهنگ#
یک نکتهی ظریف که روی سایت پرترافیک اهمیت پیدا میکند: بین لحظهای که فایل CSS جدید آپلود میشود و لحظهای که HTML جدید سرو میشود، یک بازه هست که کاربر ممکن است HTML قدیمی با CSS جدید یا برعکس بگیرد.
با نسخهگذاری، این مشکل تا حد زیادی خودبهخود حل میشود: HTML قدیمی به آدرس نسخهی قدیمی اشاره میکند و آن فایل هنوز موجود است. به شرطی که فایل قدیمی را حذف نکنید — که با روش پارامتر پرسوجو خودکار برقرار است، ولی با روش تغییر نام فایل باید حواستان باشد چند نسخهی آخر را نگه دارید.
خلاصه#
- هدر
immutableیعنی مرورگر حتی بررسی هم نمیکند؛ رفرش اجباری راهحل قابل اتکایی نیست. - بهجای خاموشکردن کش، آدرس را نسخهدار کنید.
- نسخه را از زمان تغییر فایل بگیرید تا خودکار باشد.
- کش طولانی فقط برای آدرسهای نسخهدار؛ HTML هیچوقت.
- اگر باز هم قدیمی بود، به ترتیب: کش قالب، فایل روی سرور، کش واسط، کش کد.