شعار أوسكار أوسكار كل المقالات

ضبط تكلفة Firestore: كيف تُقلّل القراءات دون أن تُبطئ تطبيقك

أغلب من ينتقل إلى Firestore يفكّر بمنطق قواعد البيانات المعتادة: احذر الاستعلامات الثقيلة والبيانات الضخمة. لكنّ Firestore يقلب هذا المنطق تماماً — الفاتورة تُحسب بعدد المستندات المقروءة، لا بحجمها ولا بتعقيد الاستعلام. مستند فيه حرف واحد ومستند فيه ألف حقل يكلّفان قراءة واحدة بالتساوي.

هذا يعني أن قرارات النمذجة التي تبدو أنيقة قد تكون أغلى قرار في مشروعك، وأن «تحسينات» معتادة لا قيمة لها هنا. هذه الدروس من تطبيق إنتاجي فيه مصادقة وأدوار وإعدادات ومنتدى — بما فيها خطأ معماري صامت كلّفنا مستخدمين لا مالاً.

ما يُحتسب قراءةً فعلاً

القاعدة الجوهرية: كل مستند يعود إليك = قراءة واحدة. استعلام يُرجع مئة مستند يكلّف مئة قراءة، لا قراءة واحدة. لا يوجد «خصم كمّية».

وتفاصيل تُغفل عادةً:

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

الكاش المحلّي الدائم: أكبر توفير بأقلّ عمل

حزمة التطوير الحديثة تتيح كاشاً محلّياً دائماً يبقى بعد إغلاق المتصفّح، ومع مدير تبويبات متعدّد يشترك في نسخة واحدة بين كل تبويبات الموقع المفتوحة:

initializeFirestore(app, {
  localCache: persistentLocalCache({
    tabManager: persistentMultipleTabManager()
  })
})

الأثر مباشر: الاستعلامات المتكرّرة — قائمة تُعاد زيارتها، إعدادات تُقرأ في كل صفحة — تُخدَّم من القرص بلا قراءة محسوبة. والفائدة الثانية مجانية: التطبيق يعمل جزئياً دون اتصال، فيرى المستخدم بياناته فوراً بدل شاشة تحميل.

ومدير التبويبات المتعدّد يعالج مشكلة عملية شائعة: بلاه، تبويبان مفتوحان على موقعك يتنافسان على قفل الكاش فيفشل أحدهما ويعمل بلا كاش أصلاً — وهو سلوك يصعب ملاحظته لأن التطبيق يعمل، فقط أغلى.

احتياط ضروري: الكاش قد يفشل

تفصيل يُنسى فيؤدّي إلى شاشة بيضاء: تفعيل الكاش الدائم قد يفشل تماماً في بيئات حقيقية — تصفّح متخفّي، أو مساحة قرص ممتلئة، أو متصفّح يمنع التخزين لموقع مضمّن في إطار. وإن لم تتعامل مع الفشل انهار تهيئة قاعدة البيانات كلّها.

الشكل الصحيح: حاول التفعيل، وإن فشل فسجّل تحذيراً واستمرّ بتهيئة عادية بلا كاش:

try { db = initializeFirestore(app, { localCache: … }) }
catch (e) { db = initializeFirestore(app, {}) }

التطبيق يصبح أغلى قليلاً لتلك الجلسة، لكنه يعمل. القاعدة: تحسين الأداء لا يجوز أن يكون شرطاً لعمل التطبيق.

المستمع الحيّ مقابل القراءة لمرّة واحدة

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

  • المستمع أرخص لبيانات تُقرأ كثيراً وتتغيّر قليلاً. مستند إعدادات يُقرأ في كل انتقال بين صفحات: بالقراءة المفردة تدفع قراءة في كل مرّة؛ بالمستمع تدفع قراءة واحدة عند الإقلاع ثم لا شيء حتى يُعدّل المستند فعلاً.
  • المستمع أغلى بمراتب لمجموعة كبيرة كثيرة التغيّر. مستمع على مجموعة فيها خمسمئة مستند يُحتسب خمسمئة قراءة عند الاتصال — وفي كل مرّة يُعاد الاتصال بعد انقطاع.

والفخّ الذي يورّط كثيرين: مستمع على استعلام مرتّب بلا حدّ. أي إضافة مستند تُعيد ترتيب النتائج فتصل تحديثات أوسع مما تتوقّع. ضع limit دائماً على أي مستمع لمجموعة.

خطأ معماري كلّفنا مستخدمين لا مالاً

هذا أغرب درس في هذا المقال، وهو معماري بحت.

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

لا خطأ في وحدة التحكّم، ولا فشل ظاهر. الميزة تعمل تماماً في الحالة التي نختبرها، ولا تعمل في الحالة الأكثر شيوعاً — الزائر الجديد. النوع الأصعب من العيوب: عيب يعمل عندك دائماً.

قاعدة: أي مستمع يخدم بيانات تُستخدم في أكثر من صفحة يجب أن يُشغَّل في مسار إقلاع مشترك، لا داخل مسار قسم واحد. واختبر دائماً بحساب زائر جديد في نافذة متخفّية، لا بحسابك المسجَّل.

وأضفنا احتياطاً ثانياً: إعادة محاولة تشغيل المستمع بعد ثانيتين إن فشل الإقلاع. السبب تسابق حقيقي بين تهيئة الكاش الدائم وأول استعلام — يقع نادراً، ونتيجته أن الميزة تغيب لتلك الجلسة كلّها. سطران يعالجان عيباً يستحيل تكراره عند الطلب.

إلغاء الاشتراك: التسريب الصامت

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

والنمط الآمن: قبل فتح مستمع، ألغِ السابق إن وُجد. ولا تفتح مستمعاً داخل دالة تُنادى مع كل إعادة رسم للواجهة — هذا أشهر مصدر لتضخّم الفاتورة بلا سبب ظاهر.

عُدّ بلا أن تقرأ

الحاجة لعرض عدد («٤٢ موضوعاً») كانت تعني جلب المستندات كلّها وعدّها — فتدفع اثنتين وأربعين قراءة لتعرض رقماً. الحلّ الصحيح دالة العدّ من الخادم:

const snap = await getCountFromServer(query(collection(db, 'topics')))
const n = snap.data().count

تكلفتها كسر ضئيل من قراءة كاملة لكل دفعة مستندات معدودة، بلا نقل أي بيانات. استخدمها في كل مكان تعرض فيه عدداً ولا تحتاج محتوى المستندات.

نمذجة تقلّل القراءات بنيوياً

  • الحقول المحسوبة مسبقاً: احفظ عدد الردود في مستند الموضوع نفسه وحدّثه عند الإضافة. عرض قائمة بعشرين موضوعاً يصبح عشرين قراءة لا عشرين استعلام عدّ.
  • مستندات ملخّص: مستند واحد يحمل ما تحتاجه صفحة الهبوط كاملاً — قراءة واحدة لأهمّ صفحة عندك.
  • ترقيم صفحات حقيقي: limit مع startAfter لا جلب المجموعة ثم تقطيعها في العميل. الثاني يدفع ثمن كل شيء ويعرض جزءاً.
  • الفهارس المركّبة: لا تكلّف قراءات إضافية، لكن كل فهرس يبطئ الكتابة قليلاً ويشغل مساحة. أنشئ ما تحتاجه استعلاماتك فقط.
  • تجنّب الاستعلام في حلقة: النمط الذي يقرأ مستند مؤلّف لكل كتاب في القائمة يضاعف قراءاتك. ضمّن الحقول المطلوبة للعرض في المستند الأصلي، ولو بتكرار محدود — التكرار المحسوب أرخص من الانضمام.

وانتبه أن قواعد الأمان تُقيَّم لكل مستند، فقد ترفض استعلاماً صحيحاً ظاهرياً — علاقة القواعد بالاستعلامات شرحناها في قواعد Firestore الآمنة عملياً.

متى تنقل العمل إلى دالة سحابية

بعض الأعمال لا يجوز أن تجري في العميل أصلاً: تجميع إحصاءات على مجموعة كبيرة، أو عمليات تحتاج صلاحيات أوسع من صلاحيات المستخدم، أو مهام مجدولة. نقلها إلى دالة سحابية يحوّل قراءات كثيرة متكرّرة من كل مستخدم إلى قراءات محدودة مرّة واحدة تُخزَّن نتيجتها في مستند ملخّص.

ونصيحة عملية في النشر: اختر منطقة الدالة قريبة من مستخدميك واذكرها صريحاً في العميل مطابقةً لما نُشرت به. عدم التطابق يعطي فشلاً غامضاً يبدو كخطأ صلاحيات وليس كذلك.

ومتى لا يكون Firestore الأداة؟ إن كانت بياناتك تتغيّر عشرات المرّات في الثانية ويحتاج الجميع رؤيتها فوراً — مثل حالة لعبة جماعية — فقاعدة البيانات في الزمن الحقيقي تُحاسب على النقل لا على القراءات، وقد تكون أرخص بمراتب. لا تختر الأداة بالشعبية بل بنموذج المحاسبة.

أسئلة شائعة

هل الكاش يجعل بياناتي قديمة؟ حزمة التطوير تخدم من الكاش ثم تحدّث من الخادم وتُبلّغ مستمعيك بالفارق. المستخدم يرى شيئاً فوراً ثم يُصحَّح — تجربة أفضل من شاشة تحميل بيضاء.

كيف أعرف عدد قراءاتي فعلاً؟ لوحة الاستخدام في كونسول المشروع تُظهر القراءات والكتابات يومياً. راقبها بعد كل ميزة جديدة — الطفرة المفاجئة تعني استعلاماً في حلقة أو مستمعاً بلا إلغاء.

هل أخزّن البيانات في العميل بدل القراءة؟ للبيانات التي لا تحتاج مزامنة — ألبوم صور محلّي مثلاً — نعم، قاعدة بيانات المتصفّح أنسب وأرخص وأخصّ بالمستخدم. لا تدفع مقابل مزامنة لا يحتاجها أحد.

ما أكثر خطأ يضخّم الفاتورة؟ مستمع بلا limit على مجموعة تنمو. يبدو رخيصاً في التطوير حين تحوي عشرة مستندات، ويصبح مكلفاً حين تحوي خمسة آلاف — والفارق أنك لن تلاحظه إلا في الفاتورة.

خلاصة

Firestore يُحاسبك على عدد المستندات لا حجمها، وهذا يقلب النمذجة: اجمع ما يُقرأ معاً، واحسب مسبقاً ما تعرضه، وعُدّ بلا أن تقرأ، وضع limit على كل مستمع. فعّل الكاش الدائم بمدير التبويبات المتعدّد — أكبر توفير بأقلّ عمل — واحرص أن فشله لا يُسقط تطبيقك.

والدرس الذي لم يكن في أي دليل قرأناه: موضع تشغيل المستمع قرار معماري لا تفصيل تنفيذي. مستمعنا كان صحيحاً تماماً، وفي المكان الخطأ — فعملت الميزة لنا وغابت عن كل زائر جديد، بلا رسالة خطأ واحدة تدلّنا.

القراءات ليست وحدها ما يُقاس — الأصول الثقيلة تكلّف وقت مستخدمك: تحسين سرعة موقعك →