التخزين المؤقت في تطبيقات الويب: أربع طبقات ومن يملك كل واحدة
أغلب أخطاء "المحتوى القديم" ليست خطأً في الشيفرة بل خلطًا بين طبقات لكل منها مالك وعمر وطريقة إبطال مختلفة.

الإجابة المختصرة
أربع طبقات مستقلة: متصفح المستخدم، شبكة التوصيل (CDN)، ذاكرة التطبيق على الخادم، وقاعدة البيانات أو ما بين التطبيق وبينها. لكل طبقة مالك مختلف وطريقة إبطال مختلفة، ومسح إحداها لا يمسّ الأخريات. تشخيص أي محتوى قديم يبدأ بتحديد الطبقة التي يجلس فيها.
في هذه الصفحة
حين يشتكي أحد أن التعديل لم يظهر، يكون السؤال الأول دائمًا: أين يجلس النسخة القديمة؟ أربعة أماكن ممكنة، ولا يشبه أحدها الآخر.
1. متصفح المستخدم
يخزّن حسب ترويسات الاستجابة: Cache-Control و ETag و Last-Modified.
- لا تملك إبطاله. لا يوجد زر يمسح ذاكرة متصفح لم تعد تتحكّم فيه.
- لذلك الملفات الثابتة تُسمّى ببصمة محتواها —
app.9f2c1a.js— وتُخزَّن سنة كاملة بـimmutable. الاسم يتغيّر مع المحتوى، فلا حاجة إلى إبطال أصلًا. - وصفحات HTML لا تُخزَّن طويلًا، لأنها هي التي تشير إلى تلك الأسماء الجديدة.
هذا الخطأ الأشيع: Cache-Control: max-age=31536000 على صفحة HTML. النتيجة مستخدم عالق على نسخة قديمة لسنة، ولا شيء تستطيع فعله من طرفك.
2. شبكة التوصيل (CDN)
نسخة مشتركة بين كل الزوار، على حافة الشبكة.
- تملك إبطالها — بالمسح حسب المسار أو حسب وسم (tag).
- تحترم
Cache-Controlغالبًا، وتقبلs-maxageالموجّه إليها وحدها دون المتصفح. stale-while-revalidateهنا هو أفضل ما في الطبقة: تقدّم النسخة القديمة فورًا وتجدّدها خلف المستخدم، فلا أحد ينتظر.
الخلط الشائع: مسح ذاكرة CDN وتوقّع أن يرى المستخدم الجديد فورًا. لن يراه إن كان متصفحه يحمل نسخة صالحة — الطبقة الأولى أمامها.
3. ذاكرة التطبيق على الخادم
نتائج محفوظة داخل عملية الخادم أو في مخزن مشترك مثل Redis.
- تملكها بالكامل، وتبطلها بالمفتاح أو بالوسم.
- خطرها أنها لكل عملية إن كانت في الذاكرة: خادمان يعنيان نسختين، ومسح إحداهما لا يمسّ الأخرى. هذا سبب "يظهر التعديل أحيانًا ويختفي أحيانًا" — أنت تتنقّل بين خادمين.
- والحل ليس إلغاءها بل جعلها مشتركة، أو ربطها بوسم يُبطَل عند الكتابة.
4. بين التطبيق وقاعدة البيانات
نتائج استعلامات محفوظة، أو ذاكرة تخزين على مستوى الاستعلام.
- الأخطر، لأن الكتابة تحدث هنا ويسهل أن ينسى المسار الكاتب إبطال ما قرأه غيره.
- القاعدة العملية: الكتابة هي الحدث الذي يبطل، لا مرور الوقت. أي كتابة تعرف ما الذي غيّرته، فتبطل ما يتعلّق به بالضبط.
طريقة التشخيص
بالترتيب، لأن كل طبقة تحجب ما بعدها:
- افتح بنافذة خاصة أو بجهاز آخر. إن ظهر الجديد فالمشكلة في متصفحك أنت.
- انظر إلى ترويسات الاستجابة. أغلب شبكات التوصيل تضيف ترويسة مثل
x-cache: HITأوage.ageكبير يعني نسخة قديمة على الحافة. - اطلب من الخادم مباشرة، متجاوزًا الشبكة. إن كان صحيحًا فالمشكلة في CDN.
- اقرأ من قاعدة البيانات مباشرة. إن كانت القيمة الجديدة هناك والتطبيق يعيد القديمة، فالمشكلة في الطبقتين الأخيرتين.
كل طبقة تخفي الطبقة التي تحتها. تشخيصها بالترتيب هو الفرق بين دقيقتين وساعتين.
قواعد تختصر أغلب المشكلات
- الملفات الثابتة: اسم ببصمة + عمر طويل +
immutable. لا إبطال أبدًا. - HTML والبيانات: عمر قصير للمتصفح، عمر طويل للـ CDN عبر
s-maxage، وإبطال بالوسم عند الكتابة. - الوسوم لا المسح الشامل. مسح كل شيء عند كل تعديل يحوّل ذاكرتك المؤقتة إلى عبء، ويجعل أول زائر بعد كل كتابة يدفع الثمن.
- لا تخزّن ما يخصّ مستخدمًا بعينه في طبقة مشتركة. هذا ليس بطء أداء بل تسريب بيانات، وهو أسوأ عطل ممكن في هذا الباب.
أسئلة شائعة
- لماذا لا يظهر التعديل بعد مسح ذاكرة CDN؟
- لأن متصفح المستخدم طبقة مستقلة أمامها. إن كان يحمل نسخة صالحة حسب ترويسات الاستجابة فلن يطلب من الشبكة أصلًا.
- ما الفرق بين max-age و s-maxage؟
- الأولى تخاطب كل من يخزّن بما فيهم المتصفح، والثانية تخاطب الذاكرة المشتركة وحدها. هذا يتيح عمرًا قصيرًا عند المستخدم وطويلًا على الحافة.
- متى أستخدم stale-while-revalidate؟
- حين يكون تقديم نسخة أقدم بثوانٍ أفضل من انتظار المستخدم. تُقدَّم النسخة المخزّنة فورًا ويجري التجديد خلفه، فلا يدفع أحد كلفة أول طلب بعد انتهاء الصلاحية.
- هل الإبطال بالوسم أفضل من المسح الكامل؟
- نعم في أغلب الحالات. المسح الكامل عند كل كتابة يجعل أول زائر بعد كل تعديل يدفع كلفة إعادة البناء، ويلغي فائدة الطبقة.
المصادر
- HTTP Caching — MDN Web Docs
- RFC 9111: HTTP Caching — IETF
- RFC 5861: HTTP Cache-Control Extensions for Stale Content — IETF
نشرته
MrBebo
محتوى تقني عربي يقدمه بهاء طه حول البرمجة، الذكاء الاصطناعي، Web2 وWeb3، تطوير التطبيقات، بناء المنتجات وأدوات المطورين.
عن المدوّنة

