آپ کے صارف کا براؤزر ٹیب تیس منٹ کے بعد فریز ہو جاتا ہے۔ UI اٹکنے لگتا ہے۔ پھر براؤزر 'out-of-memory' error کے ساتھ کریش ہو جاتا ہے۔

آپ کمپوننٹ کے کوڈ کا ایک ایک لائن کر کے جائزہ لیتے ہیں اور کچھ بھی غلط نظر نہیں آتا۔ React میں میموری لیکس (memory leaks) کا یہی سب سے پریشان کن پہلو ہے۔ بگ آپ کے JSX سنٹیکس یا آپ کے hook logic میں نہیں ہوتا۔ یہ آپ کے کمپوننٹ اور براؤزر کے garbage collector کے درمیان کے خلا میں چھپا ہوتا ہے۔ ایک بھٹکا ہوا event listener یا ایک طویل عرصے تک چلنے والا closure کلیکٹر کو میموری واپس لینے سے روک دیتا ہے۔ آپ ایک کمپوننٹ کو unmount کرتے ہیں، لیکن ایک واحد باقی رہ جانے والا ریفرنس پورے ٹری (tree) کو heap میں زندہ رکھتا ہے۔ آپ صرف کوڈ پڑھ کر ان لیکس کو نہیں ڈھونڈ سکتے۔ مسئلہ سطح کے نیچے چھپا رہتا ہے، جو آنکھوں سے نظر نہیں آتا اور یونٹ ٹیسٹ (unit tests) میں خاموش رہتا ہے۔

لیکس (Leaks) سامنے ہو کر بھی کیوں چھپے رہتے ہیں

V8 جیسے JavaScript engines میموری کو خودکار طریقے سے مینیج کرتے ہیں۔ جب روٹ (root) سے کسی آبجیکٹ تک کوئی ریفرنس پاتھ موجود نہیں ہوتا، تو انجن اس آبجیکٹ کو 'garbage' کے طور پر نشان زد کر دیتا ہے اور جگہ خالی کر دیتا ہے۔ یہ عمل تب تک اچھی طرح کام کرتا ہے جب تک کہ کوئی چھپا ہوا ریفرنس آپ کی مرضی کے خلاف زیادہ دیر تک برقرار نہ رہے۔

React میں، خطرہ اکثر کمپوننٹس اور DOM کے درمیان کی حد پر ظاہر ہوتا ہے۔ آپ کسی موڈل (modal) کے اندر window پر resize listener لگا سکتے ہیں، یا ڈیش بورڈ ویجیٹ میں WebSocket کو سبسکرائب کر سکتے ہیں۔ جب صارف موڈل بند کرتا ہے یا دوسرے صفحے پر جاتا ہے، تو کمپوننٹ unmount ہو جاتا ہے۔ اگر سبسکرپشن برقرار رہتی ہے، تو انجن گلوبل window آبجیکٹ سے لے کر آپ کے ہینڈلر (handler) تک، اور آپ کے ہینڈلر سے واپس کمپوننٹ کے closure تک ایک درست ریفرنس دیکھتا ہے۔ کمپوننٹ، اس کے props، اس کی state، اور DOM nodes کا اس کا پورا subtree میموری میں جڑا رہتا ہے۔ سینکڑوں انٹرایکشنز کے بعد، یہ جڑے ہوئے آبجیکٹس جمع ہونے لگتے ہیں۔ میموری کا استعمال ایک sawtooth pattern میں بڑھتا ہے جو کبھی مکمل طور پر نیچے نہیں آتا۔

انٹرپرائز ڈیش بورڈ کا مسئلہ

یہ سب سے زیادہ انٹرپرائز ڈیش بورڈز میں ہوتا ہے جہاں صارفین گھنٹوں ایک ہی صفحے پر رہتے ہیں۔ مانیٹرنگ پینلز، اینالیٹکس ویوز، یا ٹکٹنگ سسٹمز کے بارے میں سوچیں۔ ایک صارف ایک ڈیٹیل موڈل کھولتا ہے، ایک بڑے ڈیٹا سیٹ کو فلٹر کرتا ہے، یا سنگل پیج ایپ (single-page app) کے اندر ٹیب بدلتا ہے۔ ہر انفرادی انٹرایکشن ٹھیک محسوس ہوتا ہے۔ تاہم، وقت کے ساتھ ساتھ، یتیم (orphaned) نوڈز اور الگ شدہ (detached) لسنرز جمع ہو جاتے ہیں۔ ایپلی کیشن اس لیے سست نہیں ہوتی کہ کوئی ایک مہنگا render ہو رہا ہے، بلکہ اس لیے ہوتی ہے کیونکہ heap اتنا بڑا ہو جاتا ہے کہ بار بار اور مہنگے garbage collection pauses شروع ہو جاتے ہیں۔

اپنے useEffect hooks کے بارے میں اندازے لگانا بند کریں۔ یہ جاننے کا واحد طریقہ کہ آیا کوئی لیک موجود ہے، براہ راست heap کو ناپنا ہے۔ Chrome DevTools آپ کو یہ بصارت (visibility) فراہم کرتا ہے۔

Chrome DevTools کے ساتھ لیکس (Leaks) کا شکار کرنا

آپ کو ایک قابلِ اعادہ (reproducible) ترتیب اور چند منٹ کی بھرپور توجہ کی ضرورت ہے۔ اپنی ایپلی کیشن کو Chrome میں کھولیں، DevTools شروع کریں، اور Memory ٹیب پر جائیں۔

ایک بیس لائن (baseline) ریکارڈ کریں۔ Heap snapshot منتخب کریں اور Take snapshot پر کلک کریں۔ یہ اس وقت JavaScript heap میں موجود ہر آبجیکٹ کو کیپچر کرتا ہے اور آپ کو ابتدائی میموری دکھاتا ہے۔ یہ کام صفحہ کے اپنے ابتدائی idle state میں مستحکم ہونے کے بعد کریں، ابتدائی لوڈ کے دوران نہیں، تاکہ آپ صرف صارف کے اقدامات سے ہونے والی वृद्धि کو ناپ سکیں۔

ایکشن (action) کو ٹرگر کریں۔ بالکل وہی UI انٹرایکشن کریں جس کے بارے میں آپ کو شک ہے کہ وہ لیک کا باعث بن رہا ہے۔ ایک موڈل کھولیں اور بند کریں، ایک پیچیدہ چارٹ کو ٹوگل کریں، یا روٹ تبدیل کریں اور واپس آئیں۔ مکمل ہونے کے بعد، ایپ کو اپنی اصل بصری حالت میں واپس لائیں۔ یہ قدم انتہائی اہم ہے۔ آپ چاہتے ہیں کہ UI بالکل ویسا ہی نظر آئے جیسا کہ بیس لائن کے دوران نظر آ رہا تھا۔ اگر UI خالی نظر آنے کے باوجود heap بڑھ گیا ہے، تو آپ کے پاس لیک کا ٹھوس ثبوت ہے۔

Garbage collection کو مجبور کریں۔ Memory ٹیب میں کچرے کے ڈبے (trash can) والے آئیکن پر کلک کریں۔ یہ ایک مکمل GC سائیکل کو ٹرگر کرتا ہے اور ان عارضی آبجیکٹس کو صاف کر دیتا ہے جو جائز طور پر اگلی کلیکشن تک برقرار رہے تھے۔ جو باقی رہ جاتا ہے وہ اصل لیکس ہیں، وہ آبجیکٹس جنہیں جمع (collect) ہو جانا چاہیے تھا لیکن حادثاتی ریفرنسز کی وجہ سے زندہ رکھے گئے تھے۔

دوسرا اسنیپ شاٹ لیں۔ دوبارہ Take snapshot پر کلک کریں۔ اب آپ کے پاس ایک ہی UI حالات کے تحت لیے گئے heap کے دو فوٹوگراف ہیں۔

نتائج کا موازنہ کریں۔ ویو (view) کو Summary سے بدل کر Comparison کر دیں۔ بیس لائن کو اپنے پہلے اسنیپ شاٹ پر سیٹ کریں اور موازنہ کرنے والے اسنیپ شاٹ کو دوسرے پر۔ Comparison ویو ہر آبجیکٹ کیٹیگری کی فہرست دیتا ہے اور آپ کو ڈیلٹا (delta) دکھاتا ہے، یعنی دونوں کیپچرز کے درمیان آبجیکٹس کی تعداد میں خالص تبدیلی۔

ڈیلٹا (Delta) کے ذریعے ترتیب دیں۔ ان کیٹیگریز کو تلاش کریں جن میں نمایاں اضافہ ہوا ہو۔ خاص طور پر Detached HTMLElement اور React fiber nodes پر توجہ دیں۔ ایک detached HTML element وہ DOM node ہے جو اب ایکٹو ڈاکومنٹ ٹری سے منسلک نہیں ہے، پھر بھی کوئی JavaScript ریفرنس اسے پکڑے ہوئے ہے۔ یہ ٹھوس ثبوت (smoking guns) ہیں۔ موڈل بند کرنے یا کمپوننٹ کو unmount کرنے کے بعد ان کا موجود نہیں ہونا چاہیے۔

Retaining Path کو پڑھنا

جب آپ اسنیپ شاٹ (snapshot) میں کسی الگ شدہ (detached) ایلیمنٹ کا انتخاب کرتے ہیں، تو Chrome نیچے والے پینل میں ریٹیننگ پاتھ (retaining path) دکھاتا ہے۔ یہ پاتھ روٹ (root) سے لے کر منتخب کردہ آبجیکٹ تک ریفرنسز کی ایک زنجیر ہے۔ اسے احتیاط سے فالو کریں۔ آپ کو اکثر ایک ایونٹ لسنر (event listener)، ایک IntersectionObserver، ایک setInterval ID، یا آپ کے کمپوننٹ کی کسی مخصوص لائن کی طرف اشارہ کرنے والا کلوزر (closure) ملے گا۔

ان ناموں کو تلاش کریں جنہیں آپ پہچانتے ہوں۔ اگر آپ کو اپنے کوڈ بیس سے کسی فنکشن کے نام کے ساتھ window سے منسلک کوئی لسنر نظر آئے، تو آپ کو اینکر (anchor) مل گیا ہے۔ وہ آبجیکٹ جو اس لسنر کو تھامے ہوئے ہے، آپ کے پورے کمپوننٹ کو زندہ رکھے ہوئے ہے۔ کبھی کبھی یہ زنجیر کسی تھرڈ پارٹی لائبریری (third-party library) سے گزرتی ہے۔ ایسی صورت میں، چیک کریں کہ آیا لائبریری کسی واضح ٹیئر ڈاؤن کال (teardown call) کی توقع رکھتی ہے جسے آپ کلین اپ فنکشن (cleanup function) میں کال کرنا بھول گئے ہوں۔

بنیادی وجوہات کا حل

ایک بار جب آپ ریٹیننگ پاتھ کی شناخت کر لیتے ہیں، تو اس کا حل عام طور پر میکانکی ہوتا ہے لیکن اس کے لیے پوری ٹیم میں نظم و ضبط کی ضرورت ہوتی ہے۔

کلین اپ فنکشنز (cleanup functions) کا استعمال کریں۔ جب بھی آپ window یا document میں لسنرز شامل کریں، تو ہمیشہ useEffect میں ایک کلین اپ فنکشن ریٹرن کریں۔ اگر آپ کا effect کسی resize ایونٹ کو سبسکرائب کرتا ہے، تو کمپوننٹ کے unmount ہونے سے پہلے اس سبسکرپشن کو ختم کر دیں۔ کلین اپ اس وقت چلتا ہے جب React کمپوننٹ کو ختم (tear down) کرتا ہے، جو آپ کو بیرونی کنکشنز کو کاٹنے کے لیے ایک یقینی ہک (hook) فراہم کرتا ہے۔

ریفرنسز کو مستحکم (stabilize) کریں۔ اپنے ہینڈلرز کو useCallback میں لپیٹیں۔ اس سے یہ یقینی بنتا ہے کہ آپ removeEventListener کو بالکل وہی فنکشن ریفرنس پاس کریں جو آپ نے اصل میں addEventListener کو پاس کیا تھا۔ اگر آپ window.addEventListener('resize', () => { ... }) جیسا کوئی ان لائن فنکشن رجسٹر کرتے ہیں اور بعد میں اسے کسی دوسرے ان لائن فنکشن کے ذریعے ہٹانے کی کوشش کرتے ہیں، تو ریفرنسز میچ نہیں ہوں گے۔ لسنر منسلک رہے گا۔ اس کے اندر موجود کلوزر آپ کے کمپوننٹ اسٹیٹ کو زندہ رکھے ہوئے ہے۔ ایک مستحکم dependency array کے ساتھ useCallback اس شناخت کے عدم مطابقت (identity mismatch) کو روکتا ہے۔

گلوبل لنکس کو ختم کریں۔ یاد رکھیں کہ براؤزر کے ایونٹ ٹارگٹ آبجیکٹس، جیسے window اور document، پیج کی پوری مدت تک زندہ رہتے ہیں۔ ان سے آپ کے کمپوننٹ تک کوئی بھی ریفرنس ایک گلوبل اینکر کے طور پر کام کرتا ہے۔ لسنر کو ہٹانے سے وہ اینکر ٹوٹ جاتا ہے اور V8 انجن اگلے garbage collection سائیکل کے دوران کمپوننٹ اسٹیٹ اور DOM نوڈز کو صاف کر دیتا ہے۔

ایک ایسے موڈل (modal) پر غور کریں جو ونڈو کی چوڑائی (width) کو ٹریک کرتا ہو۔ کلین اپ کے بغیر، ہر بار جب صارف موڈل کھولتا ہے، ایک نیا لسنر منسلک ہو جاتا ہے۔ پرانے لسنرز کبھی الگ نہیں ہوتے کیونکہ وہ جن کمپوننٹ انسٹنسز سے تعلق رکھتے ہیں وہ ختم ہو چکے ہوتے ہیں، لیکن فنکشنز خود گمنام (anonymous) تھے اور کھو گئے۔ useCallback میں لپٹا ہوا ایک نامزد ہینڈلر، اور ساتھ ہی ایک کلین اپ فنکشن جو removeEventListener کو کال کرے، اس عمل کو صفائی سے مکمل کر دیتا ہے۔

ایک حقیقی سبق

React میں میموری لیکس (memory leaks) شاذ و نادر ہی کسی واضح ایرر میسج کے ساتھ ظاہر ہوتے ہیں۔ وہ ایک ایسے ٹیب کے ذریعے ظاہر ہوتے ہیں جو جتنا زیادہ کھلا رہے، اتنا ہی بھاری ہوتا جاتا ہے۔ اس بارے میں اندازہ لگانے میں وقت ضائع نہ کریں کہ کون سا ہک (hook) قصوروار ہے۔ Memory ٹیب کھولیں، garbage collection کو زبردستی چلائیں، اور اسنیپ شاٹس کا موازنہ کریں۔ Heap profiler کو آپ کو درست ریٹیننگ پاتھ دکھانے دیں۔ پھر کلین اپ فنکشن لکھیں، callback ریفرنس کو مستحکم کریں، اور گلوبل لنک کو کاٹ دیں۔ آپ کے صارفین اس حل کو براہ راست محسوس نہیں کریں گے، لیکن وہ یہ محسوس کریں گے کہ دن کے اختتام پر بھی ڈیش بورڈ ہموار طریقے سے چل رہا ہے۔