Your app runs fine for ten minutes. Then the scroll gets sticky. After half an hour the tab hits a gigabyte. Eventually the page dies with an out-of-memory error and you have no stack trace to show for it.
This is not a render performance problem. The React DevTools Profiler will look quiet because the issue is not how often components redraw. The issue is what stays alive after they unmount. A stray reference somewhere in the JavaScript heap pins an entire tree of DOM nodes, closures, and state. The browser cannot reclaim any of it, so memory climbs until the process collapses.
Reading your source code will not reveal the leak. The bug lives in the gap between what you think unmounted and what the garbage collector actually sees. V8 only frees objects that have zero retaining paths. If a rogue event listener, an uncleared observer, or a long-lived closure holds even one pointer to a fiber or a DOM node, the whole component subtree survives. You unmount a modal, but its detached nodes remain in memory because a listener on window still points to a handler defined inside that modal.
To prove where the leak lives you need to look at the heap, not the editor.
Why the Heap Tells the Truth
Chrome DevTools gives you a direct window into what the garbage collector sees. The Memory tab can record heap snapshots: complete inventories of every object, DOM node, and closure currently held in JavaScript memory. By comparing two snapshots—one before a suspected leak and one after—you can isolate exactly which objects failed to die.
This is not abstract theory. A single leaked React component can retain thousands of detached HTMLElement objects. Those objects are no longer attached to the visible document, but JavaScript references prevent them from being collected. They show up in the comparison view with the constructor name Detached HTMLElement. When you see them multiplying, you have found your leak.
The Chrome DevTools Workflow
Start clean. Close unrelated browser tabs, disable unrelated extensions, and let your application settle into a steady state. Open Chrome DevTools, switch to the Memory tab, and select Heap snapshot. Click Take snapshot. This baseline captures your starting memory footprint.
Now perform the exact user action you suspect. Open and close that heavy modal. Mount and unmount the widget. Navigate to the route and back. Once the UI has returned to its original visual state, click the trash can icon in the Memory tab. This forces a global garbage collection pass. Temporary objects from the render cycle should drop away. Anything that remains is a real candidate for a leak.
Click Take snapshot again. You now have two photographs of memory. Change the view from Summary to Comparison. Set the comparison scope to the first snapshot. The tool will show you only what changed between the two captures, stripping away the noise of the runtime.
Sort by Delta. Look for object counts that grew. Pay special attention to constructors like Detached HTMLElement, Array, Function, or even named class instances from your own codebase. A rising delta means objects were created during your action and were not collected afterward.
Tracing the Retaining Path
When you spot a leaked element, select it. The bottom panel displays the retaining path: a chain of references that explains why this object is still alive. The chain might run from a detached div up through React internal properties, into a closure, and finally land on an event listener registered inside one of your components. That final link in the chain is your line number.
This is where you move from diagnosis to root cause. If the retaining path ends at window.addEventListener, you know a global listener is holding your component hostage. If it ends at an IntersectionObserver instance, you know an observer is still watching a node that should have been garbage collected.
Common Culprits in React
Memory leaks in React usually fall into three patterns.
Orphaned global listeners. A useEffect hooks into window or document to track scroll position, key presses, or resize events. If the effect does not return a cleanup function that calls removeEventListener, the listener survives for the lifetime of the page. Because the listener is a closure, it keeps the entire component scope alive long after React has unmounted the component.
আনক্লিয়ার্ড অবজারভার (Uncleared observers)। IntersectionObserver এবং ResizeObserver অত্যন্ত শক্তিশালী, কিন্তু এগুলো React-এর নিয়ন্ত্রণের বাইরে নেটিভ রেফারেন্স তৈরি করে। আপনি যদি কোনো কম্পোনেন্টের ভেতরে একটি অবজারভার ইনস্ট্যান্সিয়েট করেন এবং ক্লিনআপ ফেজে disconnect() কল করতে ভুলে যান, তবে অবজারভারটি টার্গেট DOM নোডটিকে ধরে রাখে, এবং সেই DOM নোডটি React fibers, props এবং state-কে ধরে রাখে।
ক্লোজার ট্র্যাপস (Closure traps)। যখন আপনি একটি কম্পোনেন্টের ভেতরে কোনো ফাংশন ডিফাইন করেন এবং সেটি কোনো থার্ড-পার্টি লাইব্রেরি, গ্লোবাল ক্যাশ বা এমনকি setTimeout-এ পাস করেন, তখন সেই ফাংশনটি তার লেক্সিক্যাল স্কোপের (lexical scope) প্রতিটি ভেরিয়েবলকে ক্লোজ করে ফেলে। যদি সেই এক্সটার্নাল ওনার ফাংশনটিকে ধরে রাখে, তবে সেটি আপনার পুরো কম্পোনেন্ট স্কোপকেও সাথে নিয়ে ধরে রাখে।
কার্যকর ক্লিনআপ প্যাটার্নসমূহ (Cleanup Patterns That Actually Work)
মেমরি লিক ফিক্স করার অর্থ হলো স্ন্যাপশটে পাওয়া প্রতিটি রিটেইনিং পাথ (retaining path) বিচ্ছিন্ন করা।
useEffect থেকে সবসময় একটি ক্লিনআপ ফাংশন রিটার্ন করুন। আপনি যদি ইফেক্টের ভেতরে কোনো লিসেনার যোগ করেন, তবে সেখানেই সেটি রিমুভ করুন।
DOM বা window-তে আপনি যে কোনো হ্যান্ডলার যুক্ত করছেন তার জন্য useCallback ব্যবহার করুন। এটি ছাড়া, প্রতিটি রেন্ডার একটি নতুন ফাংশন রেফারেন্স তৈরি করে। আপনি যদি একটি রেফারেন্স দিয়ে addEventListener কল করেন এবং পরে অন্য একটি রেফারেন্স দিয়ে removeEventListener কল করেন, তবে রিমুভালটি নিঃশব্দে ব্যর্থ হবে। আসল লিসেনারটি চিরকাল window-তে থেকে যাবে। useCallback রেফারেন্সটিকে স্থিতিশীল রাখে যাতে add এবং remove হুবহু মিলে যায়।
অবজার্ভারগুলোকে একই শৃঙ্খলার সাথে হ্যান্ডেল করুন। ইফেক্টের ভেতরে অবজারভার ইনস্ট্যান্সটিকে একটি ref বা লোকাল ভেরিয়েবলে স্টোর করুন। ক্লিনআপ ফাংশনে observer.disconnect() কল করুন। কম্পোনেন্ট আনমাউন্ট (unmount) হলেই যে অবজারভারটি বন্ধ হয়ে যাবে বলে ধরে নেবেন না। এটি হয় না।
আপনার কম্পোনেন্ট যদি কোনো গ্লোবাল নেমস্পেস বা সিঙ্গেলটন সার্ভিসে কিছু পাবলিশ করে, তবে আনমাউন্ট করার সময় সেই রেফারেন্সগুলো ডিলিট করে দিন। V8 engine শুধুমাত্র তখনই মেমরি রিক্লেম (reclaim) করতে পারে যখন একটি অবজেক্ট সত্যিকার অর্থে আনরিচেবল (unreachable) হয়। window-তে কোনো হুক বা মডিউল-লেভেল Map-এ কোনো এন্ট্রি রেখে দিলে একটি অদৃশ্য ব্রিজ তৈরি হয় যা হিপ (heap) ক্রমাগত বাড়িয়ে দেয়।
আসল শিক্ষা (The Real Takeaway)
মেমরি লিক আপনার অ্যাপকে তাৎক্ষণিকভাবে ক্র্যাশ করায় না। দীর্ঘ ইউজার সেশনের সময় এগুলো একটির পর একটি ডিটাচড নোড হিসেবে জমা হতে থাকে। এর সমাধান কোনো লাইব্রেরি আপগ্রেড বা কম্পাইলার ফ্ল্যাগ নয়। এর সমাধান হলো হিপ স্ন্যাপশট (heap snapshots) দিয়ে আপনার ক্লিনআপ লজিক প্রমাণ করার অভ্যাস গড়ে তোলা।
একটি বেসলাইন নিন, সন্দেহভাজন ফ্লোটি ট্রিগার করুন, ফোর্স গারবেজ কালেকশন (garbage collection) করুন এবং তুলনা করুন। যদি ডেল্টা (delta) বৃদ্ধি পায়, তবে রিটেইনিং পাথটি পরীক্ষা করুন, যে লিসেনার বা অবজারভারটি থাকা উচিত নয় সেটি খুঁজে বের করুন এবং রেফারেন্সটি কেটে দিন। পুনরায় টেস্টটি চালান। যখন ডেল্টা স্থির থাকবে, তখন বুঝবেন আপনি এটি সমাধান করেছেন। আপনার অ্যাপ্লিকেশন রেসপন্সিভ থাকবে এবং আপনার ব্যবহারকারীরা কোনো ফ্রিজ হয়ে যাওয়া ব্রাউজার ট্যাবের কারণে তাদের কাজ হারাবেন না।
