تغيّر نمطا، وترفع الملف، وتحدّث الصفحة — لا تغيير. تجرب التحديث الإجباري، ولا شيء أيضا. تفتح نافذة التصفح الخفي وهناك يظهر صحيحا.

إذا مررت بهذا فغالبا واجهت ترويسة تخزين مؤقت خاصة ترسلها كثير من الاستضافات افتراضيا، وسلوكها يختلف جوهريا عن التخزين المؤقت المعتاد.

ماذا تفعل الترويسة immutable#

التخزين المؤقت المعتاد في المتصفح يعمل هكذا: يحتفظ بالملف، وعندما يحتاجه مرة أخرى يرسل طلبا شرطيا معناه «لدي هذه النسخة، هل تغيّرت؟». فإذا أجاب الخادم بلا يُعاد الرمز 304 ولا يُحمَّل الملف من جديد. والمهم أن طلبا قد أُرسل وأن المتصفح أتيحت له فرصة أن يعرف أن الملف تغيّر.

الآن انظر إلى هذه الترويسة:

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(). فات موضع أو اثنان، وكانا تحديدا ما «لا يتحدّث» لاحقا. إذا بنيت مساعدا كهذا فانقل كل المستهلكين إليه في الوقت نفسه.

ملاحظة عن المسار#

إذا كان المشروع خارج جذر الويب — وهذا هو الهيكل المعتاد على cPanel — فإن الدالة public_path() لا تشير بالضرورة إلى المكان الذي يُقدَّم منه الملف فعلا. تحقق من المسار الحقيقي:

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

ما الذي نمنحه تخزينا طويلا وما الذي لا نمنحه#

القاعدة البسيطة: كل ما كان عنوانه مرقّم النسخة يمكن أن يحصل على تخزين طويل. وكل ما كان عنوانه ثابتا لا ينبغي.

الموردالسياسة المقترحة
CSS وJS مع النسخة في العنوانسنة واحدة، immutable
الخطوطسنة واحدة، immutable
صورة مرفوعة باسم فريدسنة واحدة
صفحة HTMLبلا تخزين أو تخزين قصير جدا
ملفا robots وsitemapبضع ساعات

الخطأ الشائع هو وضع تخزين طويل على صفحة HTML نفسها. عندها يرى المستخدم محتوى قديما ولا ينقذه أي ترقيم للنسخ، لأن عنوان الصفحة لا يتغير.

عندما لا تُحل المشكلة بعد#

إذا أضفت ترقيم النسخ وما زلت ترى القديم، فافحص بهذا الترتيب:

1. هل يحمل HTML الناتج النسخة فعلا؟ انظر إلى مصدر الصفحة لا إلى شيفرة القالب:

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

إذا لم يكن معامل النسخة في الناتج فهذا يعني أن ذاكرة القوالب المؤقتة في لارافيل ما زالت تحتفظ بالنسخة القديمة:

Bash
php84 artisan view:clear
php84 artisan optimize:clear

2. هل تغيّر الملف على الخادم فعلا؟

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

حدث أكثر من مرة أن الملف لم يُرفع أصلا.

3. هل لديك ذاكرة وسيطة؟ إذا كان الموقع خلف شبكة توصيل محتوى فعليك مسح ذاكرتها أيضا. وفي ترويسات الاستجابة عادة إشارة تدل على أن الرد جاء من الذاكرة المؤقتة أو من الخادم الأصلي.

4. هل التخزين المؤقت لشيفرة PHP (opcache) مفعّل؟ على الاستضافة المشتركة تحتفظ ذاكرة الشيفرة في الخادم أحيانا بالقيمة القديمة لملف 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. إذا لم تكن الوحدة موجودة على الخادم وأزلت الشرط فسيعرض الموقع كله الخطأ 500 — وعلى الاستضافة المشتركة التي لا تصل فيها بسهولة إلى سجل خادم الويب يستغرق اكتشاف السبب وقتا.

وتذكّر أن تضع هذه الترويسة فقط عندما تكون العناوين مرقّمة النسخة. الترتيب مهم: أولا ترقيم النسخ، ثم التخزين الطويل. العكس يعني عدة أشهر من المحتوى القديم لدى الزوار القدامى.

افحص الضغط أيضا#

في هذا المجال هناك شيء آخر يستحق الفحص. انظر هل تُضغط الاستجابات أم لا:

Bash
curl -sI -H 'Accept-Encoding: gzip, br' https://example.com/assets/css/main.css | grep -i content-encoding

إذا كان الناتج فارغا فإن الملفات النصية تُنقل دون ضغط. وبالنسبة لملف CSS حجمه بضع عشرات من الكيلوبايت، يقلل الضغط عادة حجم النقل أضعافا، ويظهر الفرق جليا على الاتصالات البطيئة.

ميزانية الأصول: رقم يجب أن تحدده#

قرار يُستحسن اتخاذه مبكرا: ضع سقفا للحجم لكل أصل والتزم به. هذا ما لدينا:

الأصلالسقف
CSS المتجر60 كيلوبايت
JavaScript المتجر8 كيلوبايت
الخط العربي90 كيلوبايت
المكتبات الخارجيةصفر

بدون سقف كهذا يكبر ملف CSS تدريجيا ولا تأتي لحظة يقول فيها أحد «صار كبيرا أكثر من اللازم». ومع السقف تأتي اللحظة من تلقاء نفسها.

وعندما تقترب من السقف فالطريق الصحيح هو الفصل لا مزيد من الضغط: النمط الذي لا يلزم إلا في صفحات المدونة يمكن أن ينتقل إلى ملف مستقل ويُحمَّل هناك فقط. وافعل الشيء نفسه لـJavaScript — لا سبب لأن يُنزَّل سكربت لوحة الإدارة لزائر المتجر.

ملاحظة عن الخطوط#

الخط العربي عادة أكبر أصل ثابت في الصفحة، ويجب أن يُستضاف على خادمك. فالخطوط المقدمة من شبكات توصيل محتوى خارجية قد تصل بطيئة جدا أو لا تصل أصلا لبعض المستخدمين بحسب بلدهم، والنتيجة صفحة بلا نص لعدة ثوان.

استضفه بنفسك، وامنحه تخزينا مؤقتا لسنة، وضع font-display: swap في قواعد الخط ليظهر النص بخط بديل إلى أن يصل الخط الأصلي.

لا تخلط ذاكرة لارافيل المؤقتة بذاكرة المتصفح#

التباس شائع: عندما يقولون «امسح الكاش» فأي كاش يقصدون؟ في مشروع لارافيل على استضافة مشتركة توجد خمس طبقات منفصلة على الأقل، ولكل منها أمرها.

الطبقةما الذي تحتفظ بهكيف تُمسح
كاش التهيئةقيم ملف env وconfigconfig:clear
كاش المساراتقائمة المساراتroute:clear
كاش القوالبملفات Blade المجمَّعةview:clear
كاش التطبيقكل ما خزّنته بنفسكcache:clear
كاش المتصفح والوسيطالملفات الثابتةترقيم نسخة العنوان

الأمر optimize:clear يمسح الأربع الأولى معا، وهو ما تضعه عادة في سكربت النشر. لكن لا أثر له على الطبقة الخامسة — وهي بالضبط الطبقة التي يدور حولها هذا المقال.

إذا لم يتغير شيء بعد النشر، فحدد أولا أي طبقة هي:

  • تغيّر HTML الناتج لكن التنسيق لم يتغير؟ الطبقة الخامسة. ترقيم النسخ.
  • لم يتغير HTML الناتج أيضا؟ الطبقة الثالثة. كاش القوالب.
  • قيمة الإعداد قديمة؟ الطبقة الأولى. كاش التهيئة.

هذا التشخيص البسيط يوفر نصف الوقت الضائع.

نشر بلا لحظة عدم تناسق#

نقطة دقيقة تكتسب أهمية على موقع كثير الزيارات: بين لحظة رفع ملف CSS الجديد ولحظة تقديم HTML الجديد توجد فترة قد يحصل فيها المستخدم على HTML قديم مع CSS جديد أو العكس.

مع ترقيم النسخ تُحل هذه المشكلة إلى حد كبير تلقائيا: يشير HTML القديم إلى عنوان النسخة القديمة وذلك الملف ما زال موجودا. بشرط أن لا تحذف الملف القديم — وهو متحقق تلقائيا بطريقة معامل الاستعلام، أما مع طريقة تغيير اسم الملف فانتبه إلى الاحتفاظ بآخر عدة نسخ.

الخلاصة#

  • الترويسة immutable تعني أن المتصفح لا يتحقق حتى؛ والتحديث الإجباري ليس حلا يُعتمد عليه.
  • بدل تعطيل التخزين، رقّم نسخة العنوان.
  • خذ النسخة من وقت تعديل الملف لتكون تلقائية.
  • التخزين الطويل للعناوين المرقّمة فقط؛ وHTML أبدا.
  • إذا بقي القديم، فافحص بالترتيب: كاش القوالب، ثم الملف على الخادم، ثم الكاش الوسيط، ثم كاش الشيفرة.