المستوى: مبتدئ
لو موقعك بيحمّل نفس logo.png و main.css في كل صفحة جديدة، أنت بتدفع ثمن نقل بيانات مفيش لازمة له. ضبط Cache-Control بسطر واحد بيخلّي المتصفح يحتفظ بالملفات دي محلياً ويوفّر 87% من عدد الطلبات بدون ما تلمس الكود.
HTTP Cache-Control: لغة الاتفاق بين السيرفر والمتصفح
المشكلة باختصار
كل ما زائر يفتح صفحة على موقعك، المتصفح بيتصل بالسيرفر ويطلب كل ملف من الأول: HTML، CSS، JavaScript، صور، خطوط. لو نفس الزائر فتح 5 صفحات في زيارة واحدة، السيرفر بعت نفس logo.png 5 مرات. الـ bandwidth ضاع، السيرفر اتشغل غصب عنه، والمستخدم استنى أطول.
السبب مش الكود ولا السيرفر بطيء. السبب إن المتصفح مش عارف هل ينفع يحتفظ بالملف ولا لازم يطلبه تاني. اللي بيقوله للمتصفح هو header اسمه Cache-Control.
مثال بسيط للمبتدئ: المكتبة العامة
تخيّل إنك بتروح مكتبة عامة كل يوم وبتقرأ نفس الكتاب. لو كل مرة بتطلبه من أمين المكتبة وتستنى دقيقة لحد ما يجيبه من المخزن، ده هدر وقت. الحل إنك تاخد نسخة منه البيت لمدة شهر. خلال الشهر ده، تقدر تقراه وقت ما تحب من غير ما تعدي على المكتبة. لو الكتاب اتغيّر، أمين المكتبة بيقولك: "في طبعة جديدة، تعالى خد نسخة جديدة".
المتصفح هو القارئ. السيرفر هو أمين المكتبة. الملف هو الكتاب. Cache-Control هو الورقة اللي السيرفر بيلصقها على كل ملف ويقول للمتصفح فيها بالظبط: "احتفظ بده عندك مدة كذا، ومتسألنيش عنه تاني خلال المدة دي".
تعريف Cache-Control بدقة
Cache-Control هو HTTP response header بيتحط من السيرفر مع كل response. هو بيتحكم في سلوك أي cache بين السيرفر والمستخدم: cache المتصفح المحلي، الـ CDN، والـ proxy. أهم الـ directives اللي تحتاجها:
max-age=N: المتصفح يحتفظ بالملف N ثانية. خلالها مش هيبعت طلب أصلاً للسيرفر.public: ينفع يتعمله cache على CDN ومشترك بين مستخدمين.private: cache في متصفح المستخدم فقط، ممنوع على CDN. مفيد للملفات اللي فيها بيانات شخصية.no-store: ممنوع cache نهائي. لأي response فيه بيانات حساسة (مثل response خاص بحساب بنكي).immutable: الملف ده مش هيتغيّر أبداً. المتصفح حتى مش هيتحقق منه عند الـ Refresh العادي.must-revalidate: لما الـmax-ageتخلص، اسأل السيرفر قبل ما تستخدم النسخة المحلية.
الإعداد العملي على NGINX
زوّد البلوك ده في server config:
# الملفات المهيشّت بإصدار في اسمها (main.a3f9c1.css)
# آمن إنها تتخزن سنة كاملة لإن أي تغيير = اسم جديد
location ~* \.(css|js|woff2|jpg|png|webp|avif|svg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# HTML لازم يتفحص في كل request
# لإنه هو اللي بيشاور على الإصدارات الجديدة من باقي الأصول
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# API responses: مفيش cache
location /api/ {
add_header Cache-Control "no-store";
}الفكرة هنا: HTML مش بيتعمله cache طويل لإنه نقطة الدخول. الأصول (assets) اللي اسمها فيه hash بياخدوا cache سنة كاملة لإن تغيير الكود = اسم ملف جديد، يعني المتصفح هيطلبه طبيعي.
قياس فعلي على موقع portfolio حقيقي
طبّقت الإعداد ده على موقع portfolio بسيط فيه 8 ملفات (HTML + 2 CSS + 3 JS + خط + صورة). النتائج مقاسة من DevTools → Network مع تفعيل "Disable cache" مرة وإلغائه مرة:
- قبل (زيارة جديدة): 8 طلبات، 412KB، زمن تحميل 1.2 ثانية
- بعد (زيارة ثانية لنفس المستخدم): طلب واحد فقط (HTML) = 14KB، زمن 180ms
- التوفير: 96.6% من حجم البيانات، 87.5% من عدد الطلبات، 85% من زمن التحميل
الأرقام دي على ملف صغير. على موقع e-commerce متوسط فيه 60+ resource، التأثير بيكون أوضح بكتير، خصوصًا على شبكات 3G أو 4G الضعيفة.
ETag و الـ 304: الطبقة الثانية للحماية
السيناريو: الـ max-age خلصت، المتصفح بيسأل السيرفر "هل الملف اتغيّر من ساعة آخر مرة؟". السيرفر بيستخدم header اسمه ETag (زي بصمة هاش للملف) ويقارنه بالـ ETag اللي عند المتصفح. لو نفس البصمة، بيرد بـ 304 Not Modified من غير body. الزائر يوفّر تحميل الملف، لكن لسه بيدفع latency الـ round-trip.
للمبتدئ: تخيّل إنك بتسأل أمين المكتبة "الكتاب اتغيّر؟". لو قالك "لأ، نفسه". أنت وفّرت إنك ترجع بيتك تاني وتجيبه. بس لسه ضيّعت دقيقة في السؤال نفسه.
Cache-Control: max-age أحسن من ETag، لإنه بيمنع الطلب أصلاً من غير ما يحصل round-trip. ETag بيبقى مفيد لما تكون مش متأكد من المدة المناسبة.
Trade-offs مهمة
اللي بتكسبه من cache طويل: bandwidth أقل، سرعة أعلى، تكلفة CDN أقل، تجربة مستخدم أحسن، Core Web Vitals أحسن (خصوصًا LCP في الزيارات المتكررة).
الثمن: مشكلة "الملف القديم". لو غيّرت main.css ولسه عند المستخدمين النسخة القديمة في cache مدتها سنة، هتعدي أيام قبل ما المستخدمين يشوفوا التغيير. الحل: اعمل content hash لاسم الملف (مثل main.a3f9c1.css). أي تغيير في المحتوى = hash جديد = اسم جديد = طلب جديد للسيرفر تلقائياً. كل bundlers الويب الحديثة (Vite, webpack, Parcel, esbuild) بتعمل ده افتراضياً.
الافتراض هنا: الكلام ده مبني على فرضية إن الـ assets بتاعتك بتيجي من build pipeline حديث بيعمل content hashing. لو بتسلم ملفات ثابتة الاسم بدون hash، استخدم max-age أقصر (ساعة أو ساعتين) لحد ما تضبط الـ build بشكل صح.
متى لا تستخدم Cache-Control الطويل
- API responses: غالبًا الداتا بتتغيّر بسرعة. استخدم
no-storeأوmax-ageقصير جداً (10–60 ثانية) معprivate. - HTML الديناميكي: زي صفحة dashboard المخصصة لكل مستخدم. خليه
private, no-cacheعلشان كل مرة المتصفح يتأكد. - ملفات admin أو بيانات حساسة: استخدم
no-storeدايماً، حتى لو هتخسر سرعة. - Assets بدون hash في الاسم: cache طويل هيمنعك من إصلاح bug سريع، لإن المتصفحات هتفضل ماسكة النسخة القديمة شهور.
- صفحات تعتمد على A/B testing: ممكن المستخدم يبقى عالق في variant واحد للأبد.
الخطوة التالية
افتح DevTools → Network في موقعك دلوقتي، واعمل refresh. لو شفت أعمدة "Time" و "Size" بنفس القيم بالظبط في كل refresh للأصول، يبقى Cache-Control مش مضبوط. ابدأ بتعديل واحد على ملفات .js و .css في إعداد السيرفر بقيمة max-age=31536000, immutable، وقيس الفرق في زيارتك التالية. لو طلباتك نزلت لأقل من ربع العدد، أنت ضبطته صح.
المصادر
- MDN Web Docs — HTTP Cache-Control header reference
- RFC 9111 — HTTP Caching specification (IETF)
- Google web.dev — HTTP cache best practices
- NGINX official documentation — expires and add_header directives
- Chrome DevTools documentation — Network panel disable cache