تاريخ النشر: 29 أكتوبر 2026
Google Chat هو تطبيق كبير من صفحة واحدة. يترك المستخدمون تطبيق الويب مفتوحًا بانتظام في علامة تبويب المتصفّح لأيام أو أسابيع في كل مرة. في التطبيقات الطويلة الأمد، تُعدّ صحة الذاكرة من جهة العميل من أهم الاهتمامات. تتراكم عمليات الإغلاق غير المجمّعة وأشجار نموذج العناصر في المستند المنفصلة ببطء، ما يؤثر في النهاية في أداء واجهة المستخدم أو توقُّف جمع المهملات مؤقتًا أو تعطُّل علامات التبويب على الأجهزة المحدودة.
لمعالجة هذه المشكلة، أطلق فريق Google Chat مبادرة للكشف عن مشاكل الذاكرة وحلّها. استخدم الفريق أدوات مطوّري البرامج في Chrome المخصّصة للوكلاء، والتي تتضمّن خادم "بروتوكول سياق النموذج" (MCP) في "أدوات مطوّري البرامج"، لتحويل عملية يدوية صعبة إلى سير عمل مبرمَج.
التحدي: العثور على إبرة في كومة قش بحجم 500 ميغابايت
يتطلّب تحديد سبب بقاء عنصر مسرَّب في الذاكرة تتبُّع مسارات الاحتفاظ بالبيانات من خلال رمز برمجي تم تصغيره بشكل كبير. على الرغم من أنّ التحسينات الأخيرة التي تم إجراؤها على لوحة "الذاكرة" في "أدوات مطوّري البرامج في Chrome" (مثل الفلترة المتقدّمة للحواف وطلب البحث عن العناصر) تسهّل هذه العملية، إلا أنّ تنفيذها يدويًا على لقطة بحجم 500 ميغابايت قد يظل بطيئًا ومرهقًا ذهنيًا.
ويتطلّب إصلاح التسرّب نوعَين مختلفَين من المعرفة:
- يجب أن تفهم كيف يتعامل محرّك V8 JavaScript مع الرمز الثنائي المجمَّع.
- تحتاج أيضًا إلى فهم إدارة الحالة المحددة لتطبيقك ودورة حياة المكون.
يستغرق الربط بين هاتين المجموعتين من المعلومات يدويًا عادةً أيامًا من التحقيق والتعاون مع مطوّرين متعددين لكل تسريب.
الحلّ: تصفُّح الكومة باستخدام وكيل
أدرك الفريق أنّه على الرغم من قدرة النماذج اللغوية الكبيرة على الاستدلال
على البيانات المنظَّمة، فإنّها تحتاج إلى الأدوات المناسبة لمعالجة الملفات الضخمة مع
الحفاظ على استهلاك الرموز المميّزة بشكل معقول. من خلال ربط Google
Antigravity بخادم MCP المفتوح المصدر في "أدوات مطوّري البرامج في Chrome"
مع تفعيل أدوات الذاكرة (--memoryDebugging=true)، تم منح الوكيل
إمكانية الوصول الآلي المباشر إلى كومة V8.
بدلاً من أن يوسّع المطوّر يدويًا شجرات الاحتفاظ في لوحة الذاكرة في "أدوات مطوّري البرامج في Chrome"، أرسل الوكيل طلبًا إلى اللقطة مباشرةً. استخدم الفريق طلبًا مباشرًا لبدء العملية:
Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.
باستخدام مهارة تصحيح أخطاء الذاكرة، نفّذ الوكيل سير عمل منظّمًا باستخدام أدوات MCP محدّدة:
- التقاط اللقطة أو تحميلها: باستخدام أدوات مثل
take_heapsnapshotأو قراءة ملف حالي. - تحليل بنية الذاكرة الديناميكية: تم البحث عن مصادر التسريب الشائعة باستخدام
get_heapsnapshot_detailsوcompare_heapsnapshots. - تتبُّع مسارات الاحتفاظ بالبيانات: تم استخدام
get_heapsnapshot_retaining_pathsلتتبُّع شجرة الهيمنة وصولاً إلى جذور تجميع المهملات. - إنشاء الإصلاحات: تمت المقارنة بين مصادر بيانات متعددة لمسارات الاحتفاظ بالرمز المصدر للتطبيق لإنشاء تصحيحات دقيقة للغاية.
إحصاءات فنية: ثلاثة أنماط شائعة لتسرُّب الذاكرة
وقد رصد الوكيل أكثر من 30 تسريبًا مختلفًا. في ما يلي الأنماط الثلاثة الأكثر تأثيرًا التي عثر عليها الوكيل، والتي تمثّل أكبر قدر من توفير الذاكرة والأنماط المعمارية المضادة الأكثر شيوعًا في تطبيقات الويب الحديثة.
فخ تتبُّع تسلسل استدعاء الدوال البرمجية للأخطاء الثابتة
حدّد الوكيل أنّ أكبر تسرُّب منفرد كان مرتبطًا بعناصر أخطاء مخصّصة مسبقًا احتفظت بشكل دائم بحوالي 20 ميغابايت من الذاكرة. على سبيل المثال، احتفظ ثابت خطأ ثابت واحد (EMPTY_KEY_ERROR) بمساحة 14.1 ميغابايت بمفرده.
المشكلة: في محرّكات JavaScript الحديثة مثل V8، يؤدي إنشاء عنصر Error إلى التقاط لقطة من حزمة التنفيذ الحالية للمساعدة في تصحيح الأخطاء. حدثت المساحة المستعادة في الذاكرة
الهائلة بسبب هذه الآلية بالضبط:
- كان الخطأ ثابتًا عامًا: تم إنشاء مثيل له أثناء التقييم الأولي للوحدة.
- التقط محرّك JavaScript تسلسل استدعاء الدوال: كانت الدالة النشطة في تسلسل استدعاء الدوال في ذلك الوقت هي برنامج تضمين الإعداد الرئيسي للتطبيق (تعبير دالة يتم استدعاؤه على الفور أو IIFE).
- احتجزت المصفوفة الذاكرة: بما أنّ التطبيق لم يجمع الخطأ مطلقًا، احتفظت مصفوفة تتبُّع تسلسل استدعاء الدوال البرمجية الداخلية بمرجع دائم لدالة الإعداد الخاصة بالحزمة الرئيسية.
// Ineffective pattern: Instantiating Error objects at module evaluation time
export const EMPTY_KEY_ERROR = new Error("Key cannot be empty");
export function validateKey(key: string) {
if (!key) {
throw EMPTY_KEY_ERROR;
}
}
الحلّ: غيّر الفريق الرمز لإنشاء الأخطاء ديناميكيًا عند حدوثها.
إذا كان الخطأ الثابت القابل لإعادة الاستخدام ضروريًا للغاية، فإنّ محو تسلسل استدعاء الدوال البرمجية (EMPTY_KEY_ERROR.stack = '';) بشكل صريح يؤدي إلى تحرير الإطارات الداخلية التي تم التقاطها في V8 على الفور.
// Recommended: Construct errors dynamically
export function validateKey(key: string) {
if (!key) {
throw new Error("Key cannot be empty");
}
}
// Or clear the stack property if a static instance is necessary:
export const REUSABLE_ERROR = new Error("Static error");
REUSABLE_ERROR.stack = ""; // Releases the captured initialization frame
توسيع نطاق متتبِّعات الأحداث على مستوى النافذة
عثر الوكيل على مشكلة في زيادة سعة الذاكرة تتعلّق بمتتبِّعات الأحداث البالغ عددها visibilitychange. على الرغم من أنّ هذا ليس "تسرُّبًا" بالمعنى الحرفي (لأنّه يتم في النهاية تنظيف
الذاكرة عند إلغاء تحميل المكوّن)، إلا أنّه زاد من استخدام الذاكرة لأنّه كان هناك
مستمع واحد لكل رسالة على الشاشة. كشفت اللقطة عن 1,142 متتبِّع أحداث مدمج، مع 128 متتبِّع نشط مرتبط مباشرةً بكائن النافذة العام.
المشكلة: في JavaScript، تكون عمليات استدعاء متتبِّع الأحداث عبارة عن عمليات إغلاق. تحتفظ الدالة المغلقة
بالمراجع إلى نطاقها المعجمي المرفق لضمان بقاء المتغيرات التي
تحتاجها قابلة للوصول. تتبّع العميل 126 من هؤلاء المستمعين إلى
مكوّن واحد: RelativeTimeController. كل طابع زمني في واجهة مستخدم Chat
(على سبيل المثال، "قبل 5 دقائق") يدير وحدة التحكّم الخاصة به، والتي تسجّل
مستمعًا خاصًا للرؤية لتحديث الوقت. نظرًا لأن كل مستمع قام بتثبيت مثيل وحدة التحكم الخاصة به في جذر النافذة، فإن شبكة كثيفة من الإغلاقات النشطة ربطت ذاكرة كبيرة طوال عمر سلسلة المحادثات المرئية.
// Ineffective pattern: Attaching global window listeners in individual UI controllers
export class RelativeTimeController {
constructor() {
this.visibilityListener = () => this.updateTime();
window.addEventListener("visibilitychange", this.visibilityListener);
}
// If dispose() fails to run or unregister, the closure pins the controller
}
الحلّ: ركّز الفريق على أحداث النوافذ العامة. يسجّل TimerService المشترَك الآن مستمعًا واحدًا بالضبط لمدى الظهور على النافذة ويبث التحديثات لجميع وحدات التحكّم النشطة في الطوابع الزمنية. أدى هذا على الفور إلى تقليص 126
مستمعًا إلى مستمع واحد بالضبط.
// Recommended: Centralized event coordination using a shared service
export class VisibilityService {
private listeners = new Set<() => void>();
constructor() {
// Single global listener throttled for the entire application
window.addEventListener("visibilitychange", () => this.notifyAll());
}
subscribe(callback: () => void): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback); // Unsubscribe handle
}
}
حِزم تطوير البرامج (SDK) التابعة لجهات خارجية وأُطر iframe المضمّنة التي تحتفظ بالبيانات
عُقد DOM المنفصلة هي عناصر تمت إزالتها من شجرة المستندات ولكن لا يزال تتم الإشارة إليها بواسطة متغيّر JavaScript. إذا احتفظ متغيّر جذر بعنصر واحد في شجرة نموذج العناصر في المستند المنفصلة، لا يمكن استرداد الشجرة المنفصلة بأكملها.
المشكلة: عندما كان المستخدمون يتنقّلون بين مساحات المحادثات، لاحظ الوكيل أنّ أشجار نموذج العناصر في المستند المنفصلة بالكامل ظلّت في الذاكرة. وتتبّع مسار الاحتفاظ بالبيانات وصولاً إلى مشغّل وسائط مضمّن في واجهة مستخدم مساحة المحادثة. عند إلغاء تحميل مساحة المحادثة، تتم إزالة مكوّن واجهة المستخدم من الصفحة، ولكن لا يتم مطلقًا تدمير حزمة تطوير البرامج (SDK) لمشغّل الوسائط الأساسي بشكل صريح. احتفظت المكتبة الخارجية بقاموس عام داخلي لمعرّفات iframe النشطة، ما أبقى DOM المنفصل نشطًا.
الحلّ: تأكَّد من أنّ جميع إطارات iframe المضمّنة والتطبيقات المصغّرة التابعة لجهات خارجية مرتبطة بدورة حياة التخلص من المكوّنات. يؤدي استدعاء طريقة .destroy() بشكل صريح في المشغّل إلى محو الإدخال من سجلّ حزمة تطوير البرامج (SDK) العام والسماح بجمع المهملات من نموذج العناصر في المستند (DOM).
// Recommended: Explicit teardown of embedded frames and SDK instances
export class MediaPlayerController {
private player: ThirdPartyPlayer | null = null;
dispose(): void {
if (this.player) {
this.player.destroy(); // Clears entry from global SDK registry
this.player = null;
}
}
}
أنماط إضافية
على الرغم من أنّ الأنماط الثلاثة المذكورة سابقًا كانت الأكثر تأثيرًا من حيث الميغابايت التي تم توفيرها، رصد الوكيل أيضًا أكثر من 27 تسرُّبًا آخر وأصلحها. وتشمل هذه الميزات ما يلي:
- بيانات قياس مصفوفة الصور عن بُعد: حيث تم الاحتفاظ بشجرات نموذج العناصر في المستند المنفصلة من خلال مصفوفات تشخيصية مخصّصة تتتبّع مقاييس الصور في الجزء المرئي من الصفحة
- أخطاء في مراحل نشاط مكوّنات إطار العمل: حيث تم تجاوز إلغاء تحميل العناصر الرئيسية الأصلية للقوائم المنسدلة المخصّصة التي تم رفعها إلى نص المستند
- فخ إغلاق كائن الوسيطات الضمنية: حيث تلتقط عمليات الإغلاق الداخلية عن غير قصد مصفوفة الوسيطات الكاملة للدوال الخارجية
التأثير: إدارة الذاكرة بشكل استباقي كسير عمل
من خلال تزويد وكيل الترميز بـ "أدوات مطوّري البرامج في Chrome المخصّصة للوكلاء"، تمكّن فريق Google Chat من تنفيذ مهمة متخصّصة للغاية بشكل آلي. انخفض الوقت المستغرَق في تتبُّع تسرُّب الذاكرة المعقّد من أيام من التنقّل اليدوي في الذاكرة الديناميكية إلى دقائق من التحليل الآلي.
على غرار تنفيذ P2ER لـ "التحقّق التجريبي الإلزامي" لفرض استقرار واجهة المستخدم، يمكن للفِرق اعتماد نهج تدقيق الذاكرة هذا كإجراء وقائي استباقي. غالبًا ما يتجاهل المطوّرون مشاكل الذاكرة، مفترضين أنّها تحدث فقط في التطبيقات الكبيرة أو المعقّدة. ومع ذلك، كما توضّح دراسة الحالة هذه، يمكن أن تتراكم الأنماط الشائعة، مثل متتبِّعات الأحداث العادية أو الأخطاء الثابتة، بدون أن يتم رصدها. إنّ دمج أدوات مثل "أدوات مطوّري البرامج في Chrome المخصّصة للوكلاء" في دورة التطوير اليومية يحوّل الذكاء الاصطناعي من أداة أساسية لإنشاء الرموز البرمجية إلى أداة موثوقة لضمان الجودة ترصد الأخطاء البرمجية الصامتة قبل وصولها إلى مرحلة الإنتاج.