# CSS جدید به مرورگر نمی‌رسد؛ حتی با Ctrl+Shift+R

- منبع: https://di9.ir/blog/%da%a9%d8%b4-immutable-%d9%88-%d9%86%d8%b1%d8%b3%db%8c%d8%af%d9%86-css
- سایت: دیباگ
- نویسنده: danidiba
- تاریخ انتشار: 2026-08-08
- آخرین به‌روزرسانی: 2026-08-08

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

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

## هدر 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) . &#039;?v=&#039; . filemtime($file);
    }
}`

و در قالب:

HTMLکپی`&lt;link rel=&quot;stylesheet&quot; href=&quot;{{ \App\Support\Asset::url(&#039;assets/css/main.css&#039;) }}&quot;&gt;`

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

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

### نکته‌ی مسیر#

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

PHPکپی`$file = base_path(&#039;public/&#039; . ltrim($path, &#039;/&#039;));`

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

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

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

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

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

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

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

Bashکپی`curl -s https://example.com | grep -o &#039;assets/css/&#91;^&quot;]*&#039;`

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

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کپی`&lt;IfModule mod_headers.c&gt;
  &lt;FilesMatch &quot;\.(css|js|woff2|png|jpe?g|webp|avif|svg)$&quot;&gt;
    Header set Cache-Control &quot;public, max-age=31536000, immutable&quot;
  &lt;/FilesMatch&gt;
&lt;/IfModule&gt;`

**شرط `IfModule` را حذف نکنید.** اگر ماژول روی سرور نباشد و شرط را برداشته باشید، کل سایت خطای ۵۰۰ می‌گیرد — و روی هاست اشتراکی که به لاگ وب‌سرور دسترسی راحت ندارید، پیدا کردن علتش وقت می‌برد.

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

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

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

Bashکپی`curl -sI -H &#039;Accept-Encoding: gzip, br&#039; 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 هیچ‌وقت.

- اگر باز هم قدیمی بود، به ترتیب: کش قالب، فایل روی سرور، کش واسط، کش کد.