הפסיקו לדלוף זיכרון עם WeakMap
תיקון קוד חלש באמצעות הפניות חלשות!
אתה מכיר את ההרגשה הזו כשאתה משנה שורת קוד אחת ורואה את השימוש בזיכרון יורד ב-50%? חוויתי את הרגע הזה כשצפיתי ב-Chrome DevTools Performance Monitor בזמן שאפליקציית דשבורד עברה מדימום של 100MB לשעה לריצה נקייה במשך אחר צהריים שלם.
השינוי בשורה אחת: new Map() הפך ל-new WeakMap().
זה הכל. אותו API surface, אותו דפוס שימוש, התנהגות שונה לחלוטין מתחת למכסה המנוע. אבל להבין למה זה עובד פירושו להבין משהו שרוב מפתחי JavaScript אף פעם לא חושבים עליו: מה קורה כשאף אחד לא מסתכל על הנתונים שלך יותר.
כשהפניות הופכות לעוגנים
Map רגיל ב-JavaScript מתייחס למפתחות שלו כמו למטען יקר. ברגע שאתה מכניס משהו לשם, ה-Map יחזיק בו באחיזת ברזל. ה-Garbage Collector רואה את הקשר הזה וחושב, “ברור שהם עדיין צריכים את האובייקט הזה, עדיף לא לגעת בו.”
האינסטינקט המגן הזה הופך לבעיה כשאתה מאחסן מטא-דאטה על דברים זמניים. צמתי DOM שמוסרים. הפעלות משתמש שפגות תוקף. מופעי רכיבים שמתפרקים. ה-Map לא יודע שהאובייקטים האלה סיימו להיות שימושיים. הוא רק יודע שיש לו הפניה, אז הוא שומר אותם בחיים.
const cache = new Map();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// The element is gone from the DOM, but cache is keeping it in memoryה-Garbage Collector לא יכול לנקות את element כי cache עדיין מצביע עליו. זה נקרא ‘strong reference’, ובאפליקציות Single Page App ארוכות טווח, זה הופך לדליפה שבסופו של דבר מקרסקת את הדפדפן.
WeakMap משנה את הכללים
WeakMap עובד אחרת. הוא מתייחס למפתחות שלו כאזרחים זמניים ולא כתושבי קבע. כשאתה מאחסן משהו ב-WeakMap, אתה בעצם אומר: “אני רוצה לשייך את הנתונים האלה לאובייקט הזה, אבל אני לא רוצה להיות הסיבה שהוא נשאר בחיים.”
אם הדבר היחיד שמחזיק אובייקט בזיכרון הוא WeakMap, ה-Garbage Collector רשאי לקחת אותו. כשהאובייקט נעלם, הרשומה ב-WeakMap נעלמת איתו. אין צורך בניקוי ידני.
const cache = new WeakMap();
function trackClick(element) { cache.set(element, { clicks: 0 });}
document.body.removeChild(element);// The element gets Garbage Collected// The cache entry vanishes automaticallyהרצתי benchmark שיצר 100,000 צמתי DOM, אחסן מטא-דאטה על כל אחד, ואז הסיר את כולם. עם Map, הדפדפן החזיק 150–200MB. עם WeakMap, זה ירד ל-70–80MB. אותו קוד, אותה פונקציונליות, חצי מטביעת הזיכרון.
מה שאתה מוותר עליו
ל-WeakMap יש אילוצים שמרגישים כמו מגבלות עד שאתה מבין שהם בדיוק מה שגורם לקסם לעבוד.
אי אפשר לעבור בלולאה על WeakMap. אין forEach, אין keys(), אין values(). זה הגיוני כשחושבים על זה: ה-Garbage Collector עלול למחוק רשומה באמצע הלולאה שלך. אתה באמת רוצה להתמודד עם זה?
אי אפשר לבדוק את הגודל. אין מאפיין .size, אין .length. שוב, זה יעד נע. המספר יכול להשתנות בין הרגע שבו אתה שואל לרגע שבו אתה מקבל תשובה.
מפתחות חייבים להיות אובייקטים. אין מחרוזות, אין מספרים, אין פרימיטיבים. זה יסודי לאופן שבו רפרנסים חלשים עובדים: לערכים פרימיטיביים אין זהות שאפשר לעקוב אחריה בנפרד מהערך עצמו.
אלה לא באגים. זה העיצוב. WeakMap בנוי למשימה אחת ספציפית: לצרף מטא-דאטה לאובייקטים מבלי למנוע מהם להתנקות. אם אתה צריך איטרציה, מפתחות פרימיטיביים או ספירת רשומות, כנראה שאתה פותר בעיה אחרת וכדאי להשתמש ב-Map רגיל.
איפה זה באמת עוזר
תבנית “הנתונים הפרטיים” הייתה מקרה השימוש המקורי של WeakMap, עוד לפני של-JavaScript היו שדות #private. ספריות היו יוצרות WeakMap מחוץ למחלקה ומשתמשות בו כדי לאחסן נתונים שלא אמורים להיות נגישים על המופע.
const privateData = new WeakMap();
class User { constructor(name) { privateData.set(this, { name }); }
getName() { return privateData.get(this).name; }}כשמופע של User נאסף על ידי ה-Garbage Collector, הנתונים הפרטיים נעלמים איתו. אין צורך בקוד ניקוי.
מימוזציה היא התאמה טבעית נוספת, במיוחד כשאתה שומר תוצאות במטמון על בסיס קלטים שהם אובייקטים ולא ערכים פרימיטיביים. אם החישוב היקר שלך מקבל אובייקט תצורה כקלט, WeakMap אומר שאתה לא צריך לדאוג שהמטון ישרוד את התצורות.
const cache = new WeakMap();
function expensiveCalc(obj) { if (cache.has(obj)) return cache.get(obj);
const result = heavyMath(obj); cache.set(obj, result); return result;}המטמון חי רק כל עוד האובייקטים שנשמרים במטמון חיים. ברגע ש-obj לא מופנה משום מקום אחר, גם התוצאה השמורה וגם הערך במטמון נעלמים יחד.
מתי להשתמש בו
דליפות זיכרון באפליקציות ווב מודרניות מגיעות בדרך כלל מהפניות מיושנות לדברים שהיו צריכים להתנקות. אם אתה בונה משהו שפועל לאורך זמן, לוח מחוונים שנשאר פתוח כל היום, אפליקציית צ’אט שרצה במשך שעות, פאנל ניהול שמעולם לא מתרענן, אתה צריך לחשוב על מה שקורה לנתונים ישנים.
WeakMap שימושי במיוחד כשאתה משייך נתונים לצמתי DOM, מופעי רכיבים, או כל אובייקט שאינך שולט על אורך חייו. אם אתה מאחסן משהו על בסיס הפניה וההפניה הזו עלולה להיעלם, WeakMap הופך את הניקוי להרבה יותר פשוט.
Map רגיל הוא עדיין הבחירה הנכונה כשאתה בונה מטמון אמיתי עם מדיניות פינוי, כשאתה צריך לעבור על הרשומות, כשאתה משתמש במפתחות פרימיטיביים, או כשהנתונים עצמם הם מה שחשוב ולא הקשר שלהם לאובייקט.
הדבר הנחמד ב-WeakMap הוא שבדרך כלל ברור מתי אתה צריך אותו. אם אתה מוצא את עצמך כותב קוד ניקוי כדי להסיר רשומות ממפה כשהאובייקטים נהרסים, זה סימן. אם אתה מודאג מזיכרון שגדל ללא גבול כי אתה לא בטוח מתי למחוק דברים, זה סימן נוסף.
לפעמים התכונה הטובה ביותר היא כזו שפשוט עובדת בלי שאתה צריך לחשוב עליה.