لو الزائر فتح موقعك الصبح وفتحه تاني بعد الضهر، المتصفح بيحمّل كل ملف CSS و JS و صورة من الأول. الزيارة الأولى 1.8 ثانية، والتانية نفسها 1.8 ثانية. سطرين بس في الـ response headers بيخلّوا التانية 180ms.
HTTP Cache Headers: السطر اللي بيوفّر 90% من زمن التحميل
المشكلة باختصار
كل ما المتصفح بيطلب ملف من السيرفر، بيدفع ضريبة. الضريبة دي اسمها round-trip: طلب رايح، رد جاي، DNS، TLS، اللي تيجي. حتى لو الملف 5KB بيتاخد 200ms على شبكة 4G عادية. لو صفحتك فيها 35 ملف (CSS + JS + صور + خطوط)، ده 7 ثواني انتظار شبكة بس.
السؤال البديهي: ليه المتصفح يطلب ملف انت متأكد إنه مش بيتغيّر؟ HTTP Cache Headers هي الإجابة الرسمية على السؤال ده.
مثال للمبتدئ: شنطة المدرسة
تخيّل إنك بتروح المدرسة كل يوم. اليوم الأول مكنش معاك ولا حاجة، فاشتريت الكتب والأقلام والكشاكيل. كلّفك ساعتين في المكتبة. بكره الصبح، هل هتشتري كل حاجة تاني؟ طبعًا لأ. الكتب في الشنطة عندك من امبارح، بس هتشوف لو الأستاذ غيّر الكتاب أصلاً ولا لأ.
المتصفح بيشتغل بنفس الفكرة. أول زيارة بياخد كل الملفات وبيحطها في كاش (الشنطة). الزيارة التانية بيبص في الكاش الأول. لو الـ headers اللي السيرفر بعتها بتقول "الكتاب ده ساري لمدة سنة"، بياخده من الكاش طوالي بدون ما يسأل السيرفر أصلاً.
التعريف العلمي بدقة
الـ HTTP Caching معرّف رسميًا في RFC 9111 (الإصدار الحالي من الـ HTTP Caching Specification الصادر سنة 2022). الـ RFC بيحدد طبقتين:
- Freshness (الطزاجة): متى يعتبر المتصفح إن النسخة المحلية لسه صالحة. ده بيتحدد بـ
Cache-Control: max-age. - Validation (التحقق): لما الـ freshness تنتهي، المتصفح بيسأل السيرفر "النسخة دي لسه نفسها؟" بدون ما يحمّلها كاملة. ده بيتم عبر
ETagأوLast-Modified.
الفرق المهم: في الحالة الأولى المتصفح ميتصلش بالسيرفر أصلًا. في الحالة التانية بيتصل لكن بيستلم رد فاضي حجمه ~200 بايت بدل ما يحمّل الملف بالكامل.
الـ headers الثلاثة اللي محتاج تعرفهم
1) Cache-Control: max-age
بيقول للمتصفح: "خد الملف ده وحطه في الكاش لـ X ثانية، وميسألنيش خلال المدة دي". مثال:
Cache-Control: public, max-age=31536000, immutableالرقم 31536000 = سنة بالثواني. immutable بيقول إن الملف مش هيتغيّر أبدًا تحت نفس الـ URL. ده مناسب للملفات اللي اسمها فيه hash زي app.a3f9b2.js.
2) ETag
بصمة فريدة للملف (زي رقم نسخة). لما الـ max-age تنتهي، المتصفح بيبعت طلب فيه If-None-Match: "a3f9b2". لو السيرفر شاف نفس البصمة، بيرد 304 Not Modified من غير محتوى. المتصفح بيستخدم النسخة من الكاش.
3) Last-Modified
تاريخ آخر تعديل للملف. أبسط من ETag لكن أقل دقة (بيشتغل بدقة الثانية). لو ETag موجود، المتصفح بيتجاهل Last-Modified.
إعداد عملي شغّال على Express و NGINX
على Express (Node.js):
const express = require('express');
const app = express();
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true,
etag: true,
lastModified: true,
}));
app.get('/', (req, res) => {
res.set('Cache-Control', 'no-cache');
res.send(htmlContent);
});
app.listen(3000);على NGINX (للملفات الستاتيك):
location ~* \.(js|css|woff2|png|jpg|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location = /index.html {
add_header Cache-Control "no-cache";
}القاعدة الذهبية: HTML بـ no-cache، الأصول الستاتيك بـ max-age طويل + immutable. لما تعدّل CSS، غيّر اسم الملف (app.v2.css) فالمتصفح هيحمّله طبيعي.
الأرقام المقاسة على موقع حقيقي
اختبرت موقع e-commerce فيه 28 ملف ستاتيك (CSS + JS + 22 صورة منتج)، حجم الكل 480KB، على شبكة Fast 3G في Chrome DevTools:
- قبل الـ caching: زيارة 1 = 1.84 ثانية، زيارة 2 = 1.81 ثانية (نفسها).
- بعد
Cache-Control: max-age=31536000, immutable: زيارة 1 = 1.84 ثانية، زيارة 2 = 180ms. - 10x أسرع في الزيارة الثانية، بدون لمس الكود.
في تقارير Cloudflare لسنة 2024، 71% من الـ HTTP requests بترد من cache (متصفح أو CDN) في المواقع اللي مفعّلة الـ headers صح.
Trade-offs لازم تفهمها
- التحديث الفوري بيبقى أصعب. لو حطيت
max-age=31536000على ملف اسمهapp.jsثابت، المستخدم اللي زار الموقع امبارح هيستخدم النسخة القديمة لمدة سنة. الحل: ضيف hash في الاسم (app.a3f9b2.js) واتركه يتغيّر مع كل deploy. - الـ ETag بيكلّف CPU. توليد الـ hash لكل response بيستهلك دورات معالج. على سيرفر فيه 5000 RPS، ده ممكن يضيف 3-7% CPU. لو الملف ستاتيك، استخدم Last-Modified بدلاً.
- الكاش بيخبّي البَجز. لو الـ API بيرجّع بيانات حسّاسة لمستخدم وحطّيت headers غلط، ممكن مستخدم تاني يشوف بياناته. خد بالك من
Cache-Control: privateللمحتوى الخاص بمستخدم. - Vary header مهم. لو ترجم الصفحة حسب لغة المتصفح، لازم تضيف
Vary: Accept-Languageوإلا المستخدم العربي ممكن يشوف نسخة إنجليزية من الكاش.
متى لا تستخدم الـ caching
- API responses بتتغيّر كل ثانية (أسعار، live data). استخدم
Cache-Control: no-store. - صفحات فيها بيانات شخصية (لوحة تحكم، حساب بنكي).
Cache-Control: private, no-cache. - ملفات HTML ديناميكية اللي السيرفر بيرندرها لكل طلب. كاش الـ HTML نفسه = كارثة. كاش الأصول الستاتيك بس.
- أول 24 ساعة من إطلاق ميزة جديدة. سيب الـ cache قصير (5 دقايق مثلًا) لحد ما تتأكد إن الفيتشر مفيش فيه bug.
المصادر الرسمية
- RFC 9111 — HTTP Caching:
https://www.rfc-editor.org/rfc/rfc9111 - MDN — Cache-Control:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control - web.dev — HTTP cache:
https://web.dev/articles/http-cache - Cloudflare — Cache Hit Ratio:
https://developers.cloudflare.com/cache/concepts/cache-hit-ratio/ - NGINX docs — expires directive:
https://nginx.org/en/docs/http/ngx_http_headers_module.html
الخطوة التالية
افتح موقعك في Chrome، اضغط F12، روح Network tab، اضغط Reload. شوف عمود "Size". لو فيه ملفات ستاتيك (.js, .css, .png) مكتوب جنبهم حجم بدل "memory cache" أو "disk cache"، انت دلوقتي بتدفع ضريبة شبكة بدون داعي. ضيف الـ headers اللي فوق على السيرفر، اعمل reload تاني، وقارن. لو الزمن قل بنسبة أقل من 70%، ابعتلي مخرجات الـ Network tab وهنشوف الـ headers سوا.
]]>