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

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

هدر immutable چه می‌کند#

کش معمولی مرورگر این‌طور کار می‌کند: فایل را نگه می‌دارد، و وقتی دوباره لازمش داشت یک درخواست شرطی می‌فرستد که یعنی «این نسخه را دارم، تغییر کرده؟». اگر سرور بگوید نه، پاسخ ۳۰۴ برمی‌گردد و فایل دوباره دانلود نمی‌شود. مهم این است که یک درخواست انجام شده و مرورگر فرصت داشته بفهمد فایل عوض شده.

حالا این هدر را ببینید:

Code
Cache-Control: public, max-age=31536000, immutable

کلمه‌ی آخر معنایش این است: «تا پایان این مدت، این فایل قطعاً عوض نمی‌شود.» مرورگری که این را می‌بیند دیگر هیچ درخواستی نمی‌زند — نه شرطی، نه معمولی. فایل را مستقیم از دیسک می‌خواند.

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

اول مطمئن شوید همین است#

هدرهای واقعی را ببینید:

Bash
curl -sI https://example.com/assets/css/main.css | grep -i cache-control

اگر immutable در خروجی بود، مشکل پیدا شد.

در ابزار توسعه‌دهنده‌ی مرورگر هم در زبانه‌ی Network، ستون Size برای این فایل چیزی شبیه «disk cache» نشان می‌دهد و اصلاً درخواستی به سرور نمی‌رود. اگر تیک «Disable cache» را بزنید فایل تازه می‌آید و همین تأییدش می‌کند.

راه‌حل غلط و راه‌حل درست#

راه‌حل غلط: هدر را بردارید#

وسوسه‌انگیز است که در .htaccess هدر را خاموش کنید. کار می‌کند، ولی دارید یک بهینه‌سازی خوب را به‌خاطر یک مشکل قابل حل قربانی می‌کنید. کش طولانی روی دارایی‌های ثابت یکی از مؤثرترین کارها برای سرعت است، مخصوصاً برای بازدیدکننده‌ی برگشتی.

راه‌حل درست: آدرس را عوض کنید#

اگر مرورگر قرار است فایل را برای همیشه کش کند، پس فایل جدید باید آدرس جدید داشته باشد. دو شکل دارد:

Code
/assets/css/main.css?v=1712345678
/assets/css/main.a1b2c3.css

اولی ساده‌تر است. دومی برای شبکه‌های توزیع محتوا مطمئن‌تر است چون بعضی از آن‌ها پارامتر پرس‌وجو را در کلید کش لحاظ نمی‌کنند.

پیاده‌سازی: نسخه از زمان تغییر فایل#

مقدار نسخه باید خودکار باشد. هر چیزی که دستی باشد بالاخره فراموش می‌شود.

ساده‌ترین منبع خودکار، زمان آخرین تغییر خود فایل است:

PHP
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);
    }
}

و در قالب:

HTML
<link rel="stylesheet" href="{{ \App\Support\Asset::url('assets/css/main.css') }}">

حالا هر بار فایل عوض شود، مقدار v هم عوض می‌شود و مرورگر آن را یک منبع کاملاً جدید می‌بیند. بدون هیچ کار دستی.

تجربه‌ی ما: بعد از ساختن این کلاس، هر جا از تابع پیش‌فرض asset() استفاده شده بود را باید عوض می‌کردیم. یکی دو مورد جا ماند و دقیقاً همان‌ها بودند که بعداً «به‌روز نمی‌شوند». اگر چنین کمکی می‌سازید، همان موقع همه‌ی مصرف‌کننده‌ها را جابه‌جا کنید.

نکته‌ی مسیر#

اگر پروژه بیرون از ریشه‌ی وب است — که روی سی‌پنل ساختار معمول همین است — تابع public_path() لزوماً به جایی که فایل واقعاً سرو می‌شود اشاره نمی‌کند. مسیر واقعی را چک کنید:

PHP
$file = base_path('public/' . ltrim($path, '/'));

چه چیزی را کش طولانی بدهیم و چه چیزی را نه#

قاعده‌ی ساده: هر چیزی که آدرسش نسخه‌دار است می‌تواند کش طولانی بگیرد. هر چیزی که آدرس ثابت دارد نباید.

منبعسیاست پیشنهادی
CSS و JS با نسخه در آدرسیک سال، immutable
فونتیک سال، immutable
تصویر آپلودی با نام یکتایک سال
صفحه‌ی HTMLبدون کش یا کش خیلی کوتاه
فایل robots و sitemapچند ساعت

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

وقتی هنوز درست نشد#

اگر نسخه‌گذاری را اضافه کردید و باز هم قدیمی می‌بینید، به این ترتیب چک کنید:

۱. آیا HTML خروجی واقعاً نسخه دارد؟ منبع صفحه را ببینید، نه کد قالب را:

Bash
curl -s https://example.com | grep -o 'assets/css/[^"]*'

اگر پارامتر نسخه در خروجی نیست، یعنی کش قالب لاراول هنوز نسخه‌ی قدیمی را دارد:

Bash
php84 artisan view:clear
php84 artisan optimize:clear

۲. آیا فایل روی سرور واقعاً عوض شده؟

Bash
ls -la --time-style=full-iso ~/app/public/assets/css/main.css

چند بار پیش آمده که فایل اصلاً آپلود نشده بود.

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

۴. آیا کش کد PHP فعال است؟ روی هاست اشتراکی گاهی کش کد سرور مقدار قدیمی فایل PHP را نگه می‌دارد. معمولاً پس از چند ثانیه خودش تازه می‌شود، ولی اگر بررسی زمان فایل خاموش باشد می‌تواند تا ری‌استارت بماند. اگر تغییرات فایل PHP هم اعمال نمی‌شود، این را در نظر داشته باشید.

تنظیم هدرها در .htaccess#

اگر هاست شما این هدرها را نمی‌فرستد و می‌خواهید خودتان اضافه‌شان کنید، در .htaccess ریشه‌ی وب:

Apache
<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 را حذف نکنید. اگر ماژول روی سرور نباشد و شرط را برداشته باشید، کل سایت خطای ۵۰۰ می‌گیرد — و روی هاست اشتراکی که به لاگ وب‌سرور دسترسی راحت ندارید، پیدا کردن علتش وقت می‌برد.

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

فشرده‌سازی را هم چک کنید#

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

Bash
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 و configconfig:clear
کش مسیرفهرست مسیرهاroute:clear
کش قالبفایل‌های Blade کامپایل‌شدهview:clear
کش اپلیکیشنهر چه خودتان ذخیره کرده‌ایدcache:clear
کش مرورگر و واسطفایل‌های ثابتنسخه‌گذاری آدرس

دستور optimize:clear چهار تای اول را با هم پاک می‌کند و همان چیزی است که معمولاً در اسکریپت استقرار می‌گذارید. ولی روی لایه‌ی پنجم هیچ اثری ندارد — و دقیقاً همان لایه‌ای است که این نوشته درباره‌اش بود.

اگر بعد از استقرار چیزی تغییر نکرده، اول تشخیص دهید کدام لایه است:

  • HTML خروجی عوض شده ولی استایل نه؟ لایه‌ی پنجم. نسخه‌گذاری.
  • HTML خروجی هم عوض نشده؟ لایه‌ی سوم. کش قالب.
  • مقدار تنظیمات قدیمی است؟ لایه‌ی اول. کش پیکربندی.

این تشخیص ساده جلوی نصف وقت تلف‌شده را می‌گیرد.

استقرار بدون لحظه‌ی ناهماهنگ#

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

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

خلاصه#

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