آپ کی ایپ دس منٹ تک ٹھیک چلتی ہے۔ پھر اسکرولنگ (scroll) مشکل ہو جاتی ہے۔ آدھے گھنٹے کے بعد ٹیب ایک گیگا بائٹ تک پہنچ جاتا ہے۔ آخر کار، پیج 'out-of-memory' ایرر کے ساتھ بند ہو جاتا ہے اور آپ کے پاس دکھانے کے لیے کوئی اسٹیک ٹریس (stack trace) نہیں ہوتا۔

یہ رینڈر پرفارمنس (render performance) کا مسئلہ نہیں ہے۔ React DevTools Profiler خاموش نظر آئے گا کیونکہ مسئلہ یہ نہیں ہے کہ کمپوننٹس کتنی بار ری ڈرا (redraw) ہوتے ہیں۔ مسئلہ یہ ہے کہ ان کے ان ماؤنٹ (unmount) ہونے کے بعد کیا چیز زندہ رہ جاتی ہے۔ JavaScript heap میں کہیں موجود ایک غیر ضروری ریفرنس (reference) DOM نوڈز، کلوزرز (closures) اور اسٹیٹ (state) کے پورے درخت کو جکڑ لیتا ہے۔ براؤزر اس میں سے کسی چیز کو بھی واپس حاصل نہیں کر سکتا، اس لیے میموری تب تک بڑھتی رہتی ہے جب تک کہ پروسیس مکمل طور پر کریش نہ ہو جائے۔

آپ کے سورس کوڈ کو پڑھنے سے یہ لیکیج (leak) سامنے نہیں آئے گی۔ یہ بگ اس فرق میں چھپا ہے کہ آپ کیا سمجھتے ہیں کہ ان ماؤنٹ ہو گیا ہے اور گاربیج کلیکٹر (garbage collector) اصل میں کیا دیکھتا ہے۔ V8 صرف ان آبجیکٹس کو آزاد کرتا ہے جن کے 'retaining paths' صفر ہوں۔ اگر کوئی غیر ضروری ایونٹ لسنر (event listener)، کوئی غیر صاف شدہ (uncleared) آبزروور، یا کوئی طویل عرصے تک چلنے والا کلوزر کسی فائبر (fiber) یا DOM نوڈ کی طرف ایک پوائنٹر بھی رکھتا ہے، تو پورا کمپوننٹ سب ٹری (subtree) زندہ رہتا ہے۔ آپ ایک موڈل (modal) کو ان ماؤنٹ کرتے ہیں، لیکن اس کے ڈیٹیچڈ (detached) نوڈز میموری میں رہتے ہیں کیونکہ window پر موجود ایک لسنر اب بھی اس موڈل کے اندر ڈیفائن کیے گئے ہینڈلر کی طرف اشارہ کرتا ہے۔

یہ ثابت کرنے کے لیے کہ لیکیج کہاں ہے، آپ کو ایڈیٹر کے بجائے ہیپ (heap) کو دیکھنے کی ضرورت ہے۔

ہیپ (Heap) حقیقت کیوں بتاتا ہے

Chrome DevTools آپ کو براہ راست وہ سب کچھ دکھاتا ہے جو گاربیج کلیکٹر دیکھ رہا ہوتا ہے۔ Memory ٹیب ہیپ اسنیپ شاٹس (heap snapshots) ریکارڈ کر سکتا ہے: یعنی JavaScript میموری میں موجود ہر آبجیکٹ، DOM نوڈ اور کلوزر کی مکمل فہرست۔ دو اسنیپ شاٹس کا موازنہ کر کے—ایک مشکوک لیکیج سے پہلے اور ایک بعد میں—آپ بالکل درست طور پر یہ الگ کر سکتے ہیں کہ کون سے آبجیکٹس ختم ہونے میں ناکام رہے۔

یہ کوئی نظریاتی بات نہیں ہے۔ ایک واحد لیکیڈ React کمپوننٹ ہزاروں ڈیٹیچڈ HTMLElement آبجیکٹس کو برقرار رکھ سکتا ہے۔ وہ آبجیکٹس اب نظر آنے والے ڈاکومنٹ سے منسلک نہیں ہیں، لیکن JavaScript ریفرنسز انہیں کلیکٹ ہونے سے روکتے ہیں۔ وہ موازنہ ویو (comparison view) میں کنسٹرکٹر کے نام Detached HTMLElement کے ساتھ ظاہر ہوتے ہیں۔ جب آپ انہیں بڑھتے ہوئے دیکھیں، تو سمجھ جائیں کہ آپ کو اپنی لیکیج مل گئی ہے۔

Chrome DevTools کا ورک فلو (Workflow)

بالکل صاف ستھرے طریقے سے شروع کریں۔ غیر متعلقہ براؤزر ٹیبز بند کر دیں، غیر متعلقہ ایکسٹینشنز کو ڈس ایبل کر دیں، اور اپنی ایپلی کیشن کو ایک مستحکم حالت (steady state) میں آنے دیں۔ Chrome DevTools کھولیں، Memory ٹیب پر جائیں، اور Heap snapshot منتخب کریں۔ Take snapshot پر کلک کریں۔ یہ بیس لائن آپ کے ابتدائی میموری فٹ پرنٹ (memory footprint) کو محفوظ کر لیتی ہے۔

اب وہی صارفانہ عمل (user action) کریں جس پر آپ کو شک ہے۔ اس بھاری موڈل کو کھولیں اور بند کریں۔ ویجیٹ کو ماؤنٹ اور ان ماؤنٹ کریں۔ روٹ (route) پر جائیں اور واپس آئیں۔ جب UI اپنی اصل بصری حالت (visual state) میں واپس آ جائے، تو Memory ٹیب میں کچرے کے ڈبے (trash can) والے آئیکن پر کلک کریں۔ یہ ایک عالمی گاربیج کلیکشن پاس (global garbage collection pass) کو مجبور کرتا ہے۔ رینڈر سائیکل کے عارضی آبجیکٹس ختم ہو جانے چاہئیں۔ جو کچھ باقی رہ جائے وہ لیکیج کا حقیقی امیدوار ہے۔

دوبارہ Take snapshot پر کلک کریں۔ اب آپ کے پاس میموری کی دو تصاویر ہیں۔ ویو کو Summary سے بدل کر Comparison کر دیں۔ موازنہ کے دائرہ کار (comparison scope) کو پہلے اسنیپ شاٹ پر سیٹ کریں۔ یہ ٹول آپ کو صرف وہی دکھائے گا جو ان دو کیپچرز کے درمیان تبدیل ہوا ہے، اور رن ٹائم (runtime) کے غیر ضروری شور کو ختم کر دے گا۔

Delta کے ذریعے ترتیب دیں۔ ان آبجیکٹ کی تعداد کو دیکھیں جو بڑھ گئی ہے۔ Detached HTMLElement، Array، Function جیسے کنسٹرکٹرز، یا یہاں تک کہ اپنے کوڈ بیس سے نامزد کلاس انسٹنسز (class instances) پر خصوصی توجہ دیں۔ بڑھتا ہوا ڈیلٹا (delta) اس بات کی علامت ہے کہ آپ کے عمل کے دوران آبجیکٹس بنائے گئے تھے اور بعد میں انہیں کلیکٹ نہیں کیا گیا۔

ریٹیننگ پاتھ (Retaining Path) کا سراغ لگانا

جب آپ کو کوئی لیکیڈ ایلیمنٹ ملے، تو اسے منتخب کریں۔ نچلا پینل ریٹیننگ پاتھ (retaining path) دکھاتا ہے: ریفرنسز کی ایک زنجیر جو وضاحت کرتی ہے کہ یہ آبجیکٹ اب بھی کیوں زندہ ہے۔ یہ زنجیر ایک ڈیٹیچڈ div سے شروع ہو کر React کی اندرونی پراپرٹیز، پھر ایک کلوزر، اور آخر کار آپ کے کسی کمپوننٹ کے اندر رجسٹرڈ ایونٹ لسنر پر ختم ہو سکتی ہے۔ زنجیر کی وہ آخری کڑی آپ کا لائن نمبر ہے۔

یہ وہ مقام ہے جہاں آپ تشخیص (diagnosis) سے اصل وجہ (root cause) کی طرف بڑھتے ہیں۔ اگر ریٹیننگ پاتھ window.addEventListener پر ختم ہوتا ہے، تو آپ جانتے ہیں کہ ایک گلوبل لسنر آپ کے کمپوننٹ کو مقید کیے ہوئے ہے۔ اگر یہ کسی IntersectionObserver کے انسٹنس پر ختم ہوتا ہے، تو آپ جانتے ہیں کہ ایک آبزروور اب بھی اس نوڈ پر نظر رکھے ہوئے ہے جسے گاربیج کلیکٹ ہو جانا چاہیے تھا۔

React میں عام وجوہات

React میں میموری لیکیج عام طور پر تین پیٹرنز میں آتی ہے۔

غیر متعلقہ گلوبل لسنرز (Orphaned global listeners)۔ ایک useEffect اسکرول پوزیشن، کی پریسیز (key presses)، یا ریسائز ایونٹس کو ٹریک کرنے کے لیے window یا document کے ساتھ جڑ جاتا ہے۔ اگر اثر (effect) کوئی کلین اپ فنکشن (cleanup function) واپس نہیں کرتا جو removeEventListener کو کال کرے، تو لسنر پیج کی پوری مدت تک زندہ رہتا ہے۔ چونکہ لسنر ایک کلوزر ہے، اس لیے یہ React کے کمپوننٹ کو ان ماؤنٹ کرنے کے کافی دیر بعد تک پورے کمپوننٹ اسکوپ کو زندہ رکھتا ہے۔

غیر ختم شدہ آبزروورز۔ IntersectionObserver اور ResizeObserver طاقتور ہیں، لیکن وہ React کے کنٹرول سے باہر نیٹیو ریفرنسز (native references) تخلیق کرتے ہیں۔ اگر آپ کسی کمپوننٹ کے اندر ایک آبزروور کو انسٹینشئیٹ (instantiate) کرتے ہیں اور کلین اپ (cleanup) مرحلے میں disconnect() کو کال کرنا بھول جاتے ہیں، تو آبزروور ٹارگٹ DOM نوڈ کو تھامے رکھتا ہے، اور DOM نوڈ React fibers، props، اور state کو تھامے رکھتا ہے۔

کلوزر ٹریپس (Closure traps)۔ جب آپ کسی کمپوننٹ کے اندر ایک فنکشن ڈیفائن کرتے ہیں اور اسے کسی تھرڈ پارٹی لائبریری، گلوبل کیش (global cache)، یا یہاں تک کہ setTimeout کو پاس کرتے ہیں، تو وہ فنکشن اپنے لیکیل اسکوپ (lexical scope) میں موجود ہر ویری ایبل کو اپنے اندر قید (close over) کر لیتا ہے۔ اگر بیرونی مالک اس فنکشن کو برقرار رکھتا ہے، تو وہ آپ کے پورے کمپوننٹ اسکوپ کو بھی اپنے ساتھ برقرار رکھتا ہے۔

کلین اپ کے وہ پیٹرنز جو حقیقت میں کام کرتے ہیں

میموری لیک کو ٹھیک کرنے کا مطلب ہے اس ہر ریٹیننگ پاتھ (retaining path) کو کاٹ دینا جو آپ کو اسنیپ شاٹ (snapshot) میں ملا ہو۔

useEffect سے ہمیشہ ایک کلین اپ فنکشن ریٹرن کریں۔ اگر آپ effect میں کوئی لسنر (listener) شامل کرتے ہیں، تو اسے وہیں سے ہٹا دیں۔

DOM یا window کے ساتھ منسلک ہونے والے کسی بھی ہینڈلر کے لیے useCallback کا استعمال کریں۔ اس کے بغیر، ہر رینڈر (render) ایک نیا فنکشن ریفرنس تخلیق کرتا ہے۔ اگر آپ ایک ریفرنس کے ساتھ addEventListener کال کرتے ہیں اور بعد میں کسی دوسرے ریفرنس کے ساتھ removeEventListener کال کرتے ہیں، تو اسے ہٹانے کا عمل خاموشی سے ناکام ہو جاتا ہے۔ اصل لسنر ہمیشہ window پر موجود رہتا ہے۔ useCallback ریفرنس کو مستحکم رکھتا ہے تاکہ add اور remove بالکل ایک جیسے ہوں۔

آبزروورز کو بھی اسی نظم و ضبط کے ساتھ ہینڈل کریں۔ آبزروور کے انسٹنس (instance) کو effect کے اندر ایک ref یا لوکل ویری ایبل میں اسٹور کریں۔ کلین اپ فنکشن میں، observer.disconnect() کو کال کریں۔ یہ فرض نہ کریں کہ کمپوننٹ کو ان ماؤنٹ (unmount) کرنے سے آبزروور ختم ہو جائے گا۔ ایسا نہیں ہوتا۔

اگر آپ کا کمپوننٹ کسی گلوبل نیم اسپیس (global namespace) یا سنگلٹن سروس (singleton service) کو کچھ پبلش کرتا ہے، تو ان ماؤنٹ کے وقت ان ریفرنسز کو ڈیلیٹ کر دیں۔ V8 انجن میموری کو صرف اس وقت واپس حاصل (reclaim) کر سکتا ہے جب کوئی آبجیکٹ واقعی ناقابل رسائی (unreachable) ہو۔ window پر کوئی ہک چھوڑنا یا ماڈیول لیول کے Map میں کوئی انٹری چھوڑنا ایک ایسا غیر مرئی پل (invisible bridge) بنا دیتا ہے جو ہیپ (heap) کو بڑھاتا رہتا ہے۔

اصل حاصلِ کلام

میموری لیکز آپ کی ایپ کو فوری طور پر کریش نہیں کرتے۔ وہ طویل یوزر سیشنز کے دوران ایک وقت میں ایک الگ شدہ نوڈ (detached node) کے ذریعے جمع ہوتے رہتے ہیں۔ اس کا حل لائبریری اپ گریڈ یا کمپائلر فلیگ نہیں ہے۔ بلکہ یہ ہیپ اسنیپ شاٹس (heap snapshots) کے ذریعے اپنے کلین اپ لاجک کو ثابت کرنے کی عادت ہے۔

ایک بیس لائن (baseline) لیں، مشکوک فلو (flow) کو ٹرگر کریں، گاربیج کلیکشن (garbage collection) کو مجبور کریں، اور موازنہ کریں۔ اگر ڈیلٹا (delta) میں اضافہ نظر آئے، تو ریٹیننگ پاتھ کا معائنہ کریں، اس لسنر یا آبزروور کو تلاش کریں جو موجود نہیں ہونا چاہیے، اور ریفرنس کو کاٹ دیں۔ ٹیسٹ دوبارہ چلائیں۔ جب ڈیلٹا مستحکم (flat) رہے، تو اس کا مطلب ہے کہ آپ نے اسے حقیقت میں حل کر لیا ہے۔ آپ کی ایپلی کیشن ریسپونسو (responsive) رہے گی، اور آپ کے صارفین کا کام کسی منجمد براؤزر ٹیب کی وجہ سے ضائع نہیں ہوگا۔