תאריך פרסום: 29 באוקטובר 2026
Google Chat היא אפליקציית דף יחיד גדולה. המשתמשים משאירים את אפליקציית האינטרנט פתוחה בכרטיסייה בדפדפן למשך ימים או שבועות. באפליקציות שפועלות לאורך זמן, תקינות הזיכרון בצד הלקוח היא נושא חשוב. סגירות שלא נאספו ועצי DOM מנותקים מצטברים לאט, ובסופו של דבר משפיעים על ביצועי ממשק המשתמש, על הפסקות באיסוף אשפה או על קריסות של כרטיסיות במכשירים מוגבלים.
כדי לטפל בבעיה הזו, הצוות של Google Chat השיק יוזמה לחשיפה ולתיקון של בעיות בזיכרון. הם השתמשו בכלי פיתוח ל-Chrome לסוכני AI, שכוללים את שרת ה-MCP של כלי הפיתוח, כדי להפוך תהליך ידני קשה לתהליך עבודה אוטומטי.
האתגר: מחט בערימת שחת של 500MB
כדי לזהות בדיוק למה אובייקט שדלף נשמר בזיכרון, צריך לעקוב אחרי נתיבי השמירה דרך קוד שעבר צמצום משמעותי. השיפורים האחרונים בחלונית Memory בכלי הפיתוח ל-Chrome (לדוגמה, סינון קצוות מתקדם ושאילתות על אובייקטים) מקלים על התהליך, אבל ביצוע ידני של התהליך בתמונת מצב של 500MB עדיין יכול להיות איטי ומתיש.
כדי לתקן נזילה, צריך שני סוגים שונים של ידע:
- אתם צריכים להבין איך מנוע ה-JavaScript V8 מטפל ב-bytecode שעבר הידור.
- אתם גם צריכים להבין את ניהול המצב הספציפי של האפליקציה ואת מחזור החיים של הרכיבים.
כדי לגשר באופן ידני בין שני מקורות המידע השונים האלה, בדרך כלל נדרשים כמה ימים של חקירה ושיתוף פעולה עם כמה מפתחים לכל דליפה.
הפתרון: ניווט בערימה באמצעות סוכן
הצוות הבין שלמרות שמודלים גדולים של שפה (LLM) טובים מאוד בהסקת מסקנות מנתונים מובְנים, הם צריכים את הכלים המתאימים כדי לעבד קבצים גדולים בלי לצרוך יותר מדי טוקנים. הם חיברו את Google Antigravity לשרת MCP של כלי הפיתוח ל-Chrome בקוד פתוח, כשהכלים לניהול הזיכרון מופעלים (--memoryDebugging=true), וכך נתנו לסוכן גישה פרוגרמטית ישירה ל-V8 heap.
במקום שהמפתח ירחיב ידנית את עצי ה-retainer בחלונית Memory בכלי הפיתוח ל-Chrome, הנציג שלח שאילתה ישירות לתמונת המצב. הצוות השתמש בהנחיה פשוטה כדי להתחיל את התהליך:
Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.
באמצעות מיומנות ניפוי הבאגים בזיכרון, הסוכן הפעיל תהליך עבודה מובנה באמצעות כלים ספציפיים של MCP:
- צילום או טעינה של קובץ ה-snapshot: באמצעות כלים כמו
take_heapsnapshotאו קריאת קובץ קיים. - ניתוח מבנה הערימה: המערכת סרקה מקורות נפוצים לדליפות באמצעות
get_heapsnapshot_detailsו-compare_heapsnapshots. - מעקב אחר נתיבי שמירה: הוא השתמש ב-
get_heapsnapshot_retaining_pathsכדי לעבור על עץ הדומיננטיות בחזרה לשורשי איסוף האשפה. - יצירת תיקונים: הוא הצליב את נתיבי השמירה עם קוד המקור של האפליקציה כדי להפיק תיקונים ספציפיים מאוד.
תובנות טכניות: שלוש תבניות נפוצות של דליפת זיכרון
הסוכן זיהה יותר מ-30 דליפות שונות. שלושת הדפוסים הבאים הם המשפיעים ביותר שהסוכן מצא, והם מייצגים את החיסכון הגדול ביותר בזיכרון ואת הדפוסים האנטי-ארכיטקטוניים הנפוצים ביותר באפליקציות אינטרנט מודרניות.
מלכודת דוח הקריסות של שגיאות סטטיות
הסוכן זיהה שהדליפה הגדולה ביותר הייתה קשורה לאובייקטים של שגיאות שהוקצו מראש, ששמרו באופן קבוע כמעט 20MB של זיכרון. לדוגמה, קבוע שגיאה סטטי יחיד (EMPTY_KEY_ERROR) שמר 14.1MB בעצמו.
הבעיה: במנועי JavaScript מודרניים כמו V8, יצירת אובייקט Error מתעדת תמונת מצב של סטאק הביצוע הנוכחי כדי לעזור בניפוי באגים. הנפח העצום שמתפנה נוצר בגלל המנגנון הזה בדיוק:
- השגיאה הייתה קבוע גלובלי: היא נוצרה במהלך ההערכה הראשונית של המודול.
- מנוע ה-JavaScript תיעד את המחסנית: הפונקציה הפעילה במחסנית באותו זמן הייתה wrapper הראשי של האפליקציה (ביטוי פונקציה שמופעל באופן מיידי, או 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
התאמה לעומס (scaling) של רכיבי event listener גלובליים של חלונות
הסוכן מצא בעיה בהתאמה לעומס (scaling) של הזיכרון שקשורה ל-visibilitychange רכיבי event listener. למרות שזו לא "דליפה" של ממש (כי בסופו של דבר הם מתנקים כשהרכיב מוסר), היא הגדילה את השימוש בזיכרון כי היה מאזין אחד לכל הודעה במסך. תמונת המצב חשפה 1,142 רכיבי event listener מובנים, עם 128 רכיבי listener פעילים שמחוברים ישירות לאובייקט החלון הגלובלי.
הבעיה: ב-JavaScript, קריאות חוזרות (callbacks) של רכיבי event listener הן סגירות (closures). סגירה שומרת הפניות להיקף הלקסיקלי שמקיף אותה כדי להבטיח שהמשתנים שהיא צריכה יישארו נגישים. הסוכן עקב אחרי 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. אם רכיב אחד בעץ DOM מנותק נשמר על ידי משתנה בסיס, אי אפשר להחזיר את כל העץ המנותק.
הבעיה: כשהמשתמשים עברו בין מרחבים משותפים בצ'אט, הנציג גילה שעצי DOM שלמים מנותקים נשארו בזיכרון. הוא עקב אחרי הנתיב השמור בחזרה לנגן מדיה מוטמע בממשק המשתמש של מרחב הצ'אט. כשמרחב משותף בצ'אט נסגר, רכיב ממשק המשתמש הוסר מהדף, אבל ה-SDK של נגן המדיה הבסיסי לא הושמד באופן מפורש. ספריית הצד השלישי שמרה על מילון גלובלי פנימי של מזהי iframe פעילים, וכך שמרה על ה-DOM המנותק פעיל.
הפתרון: ודאו שכל רכיבי ה-iframe המוטמעים והווידג'טים של צד שלישי קשורים למחזורי החיים של סילוק הרכיבים. קריאה מפורשת ל-method .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 דליפות אחרות. למשל:
- טלמטריה של מערך תמונות: עצי DOM מנותקים נשמרו על ידי מערכי אבחון מותאמים אישית שעוקבים אחרי מדדי תמונות בחלק העליון של הדף (above the fold)
- באגים במחזור החיים של רכיבי Framework: תפריטים נפתחים בהתאמה אישית שהועלו לגוף המסמך עקפו את הסרת ההתקנה של ההורים המקוריים שלהם
- מלכודת סגירה של אובייקט ארגומנטים משתמעים: מצב שבו סגירות פנימיות לכדו בטעות את כל מערך הארגומנטים של פונקציות חיצוניות
ההשפעה: ניהול זיכרון יזום כחלק מתהליך העבודה
הצוות של Google Chat צייד את סוכן התכנות בכלי הפיתוח ל-Chrome לסוכני AI, וכך הפך משימה מאוד ספציפית לאוטומטית. מעקב אחרי דליפת זיכרון מורכבת, שבעבר דרש ימים של מעבר ידני על ה-heap, מתבצע עכשיו תוך דקות של ניתוח אוטומטי.
כמו ש-P2ER הטמיעו "אימות אמפירי חובה" כדי לאכוף את יציבות ממשק המשתמש, צוותים יכולים לאמץ את הגישה הזו של ביקורת זיכרון כמעקה בטיחות פרואקטיבי. מפתחים נוטים להתעלם מבעיות בזיכרון, כי הם מניחים שהן קורות רק באפליקציות גדולות או מורכבות מאוד. עם זאת, כפי שמוצג במקרה לדוגמה הזה, דפוסים נפוצים כמו רכיבי event listener רגילים או שגיאות סטטיות יכולים להצטבר בלי שנשים לב. כשמשלבים כלים כמו כלי פיתוח ל-Chrome לסוכני AI במחזור הפיתוח היומי, ה-AI הופך ממחולל קוד בסיסי לשער איכות אמין שמזהה רגרסיות שקטות לפני שהן מגיעות לסביבת הייצור.