ਤੁਹਾਡੀ ਐਪ ਦਸ ਮਿੰਟਾਂ ਤੱਕ ਠੀਕ ਚੱਲਦੀ ਹੈ। ਫਿਰ ਸਕ੍ਰੋਲ (scroll) ਅਟਕਣ ਲੱਗਦਾ ਹੈ। ਅੱਧੇ ਘੰਟੇ ਬਾਅਦ ਟੈਬ ਇੱਕ ਗੀਗਾਬਾਈਟ ਤੱਕ ਪਹੁੰਚ ਜਾਂਦਾ ਹੈ। ਅੰਤ ਵਿੱਚ, ਪੇਜ 'out-of-memory' ਐਰਰ ਦੇ ਨਾਲ ਬੰਦ ਹੋ ਜਾਂਦਾ ਹੈ ਅਤੇ ਤੁਹਾਡੇ ਕੋਲ ਦਿਖਾਉਣ ਲਈ ਕੋਈ ਸਟੈਕ ਟ੍ਰੇਸ (stack trace) ਨਹੀਂ ਹੁੰਦਾ।

ਇਹ ਰੈਂਡਰ ਪਰਫਾਰਮੈਂਸ (render performance) ਦੀ ਸਮੱਸਿਆ ਨਹੀਂ ਹੈ। React DevTools Profiler ਸ਼ਾਂਤ ਦਿਖਾਈ ਦੇਵੇਗਾ ਕਿਉਂਕਿ ਸਮੱਸਿਆ ਇਹ ਨਹੀਂ ਹੈ ਕਿ ਕੰਪੋਨੈਂਟ ਕਿੰਨੀ ਵਾਰ ਰੀਡ੍ਰੌ (redraw) ਹੁੰਦੇ ਹਨ। ਸਮੱਸਿਆ ਇਹ ਹੈ ਕਿ ਉਹਨਾਂ ਦੇ unmount ਹੋਣ ਤੋਂ ਬਾਅਦ ਕੀ ਜਿਉਂਦਾ ਰਹਿੰਦਾ ਹੈ। JavaScript heap ਵਿੱਚ ਕਿਤੇ ਕੋਈ ਇੱਕ ਵਾਧੂ ਰੈਫਰੈਂਸ (reference) DOM nodes, closures, ਅਤੇ state ਦੇ ਪੂਰੇ ਟ੍ਰੀ ਨੂੰ ਫਸਾ ਲੈਂਦਾ ਹੈ। ਬ੍ਰਾਊਜ਼ਰ ਇਸ ਵਿੱਚੋਂ ਕੁਝ ਵੀ ਵਾਪਸ ਨਹੀਂ ਲੈ ਸਕਦਾ, ਇਸ ਲਈ ਮੈਮੋਰੀ ਉਦੋਂ ਤੱਕ ਵਧਦੀ ਰਹਿੰਦੀ ਹੈ ਜਦੋਂ ਤੱਕ ਪ੍ਰੋਸੈਸ (process) ਬੰਦ ਨਹੀਂ ਹੋ ਜਾਂਦਾ।

ਤੁਹਾਡਾ ਸੋਰਸ ਕੋਡ (source code) ਪੜ੍ਹਨ ਨਾਲ ਲੀਕ (leak) ਦਾ ਪਤਾ ਨਹੀਂ ਲੱਗੇਗਾ। ਇਹ ਬੱਗ ਉਸ ਖਾਲੀ ਥਾਂ ਵਿੱਚ ਹੁੰਦਾ ਹੈ ਜੋ ਤੁਹਾਡੇ ਅਨੁਮਾਨਿਤ unmount ਹੋਣ ਅਤੇ garbage collector ਦੁਆਰਾ ਅਸਲ ਵਿੱਚ ਦੇਖੇ ਜਾਣ ਦੇ ਵਿਚਕਾਰ ਹੁੰਦੀ ਹੈ। V8 ਸਿਰਫ਼ ਉਹਨਾਂ ਆਬਜੈਕਟਾਂ ਨੂੰ ਫ੍ਰੀ ਕਰਦਾ ਹੈ ਜਿਨ੍ਹਾਂ ਦੇ ਰਿਟੇਨਿੰਗ ਪਾਥ (retaining paths) ਜ਼ੀਰੋ ਹੁੰਦੇ ਹਨ। ਜੇਕਰ ਕੋਈ rogue event listener, ਕੋਈ ਅਣ-ਕਲੀਅਰ (uncleared) observer, ਜਾਂ ਕੋਈ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲਣ ਵਾਲਾ closure ਕਿਸੇ fiber ਜਾਂ DOM node ਨੂੰ ਇੱਕ ਪੁਆਇੰਟਰ (pointer) ਵੀ ਫੜ ਕੇ ਰੱਖਦਾ ਹੈ, ਤਾਂ ਪੂਰਾ component subtree ਜਿਉਂਦਾ ਰਹਿੰਦਾ ਹੈ। ਤੁਸੀਂ ਇੱਕ modal ਨੂੰ unmount ਕਰਦੇ ਹੋ, ਪਰ ਉਸਦੇ detached nodes ਮੈਮੋਰੀ ਵਿੱਚ ਰਹਿੰਦੇ ਹਨ ਕਿਉਂਕਿ window 'ਤੇ ਇੱਕ listener ਅਜੇ ਵੀ ਉਸ modal ਦੇ ਅੰਦਰ ਪਰਿਭਾਸ਼ਿਤ ਹੈਂਡਲਰ (handler) ਵੱਲ ਇਸ਼ਾਰਾ ਕਰਦਾ ਹੈ।

ਇਹ ਸਾਬਤ ਕਰਨ ਲਈ ਕਿ ਲੀਕ ਕਿੱਥੇ ਹੈ, ਤੁਹਾਨੂੰ editor ਦੀ ਨਹੀਂ, ਬਲਕਿ heap ਦੀ ਦੇਖਣ ਦੀ ਲੋੜ ਹੈ।

Heap ਸੱਚ ਕਿਉਂ ਦੱਸਦਾ ਹੈ

Chrome DevTools ਤੁਹਾਨੂੰ ਉਹ ਦੇਖਣ ਲਈ ਇੱਕ ਸਿੱਧਾ ਰਸਤਾ ਦਿੰਦਾ ਹੈ ਜੋ garbage collector ਦੇਖਦਾ ਹੈ। Memory tab heap snapshots ਰਿਕਾਰਡ ਕਰ ਸਕਦਾ ਹੈ: JavaScript ਮੈਮੋਰੀ ਵਿੱਚ ਇਸ ਸਮੇਂ ਰੱਖੇ ਗਏ ਹਰ ਆਬਜੈਕਟ, DOM node, ਅਤੇ closure ਦੀ ਪੂਰੀ ਸੂਚੀ। ਦੋ snapshots ਦੀ ਤੁਲਨਾ ਕਰਕੇ—ਇੱਕ ਸ਼ੱਕੀ ਲੀਕ ਤੋਂ ਪਹਿਲਾਂ ਅਤੇ ਇੱਕ ਬਾਅਦ ਵਿੱਚ—ਤੁਸੀਂ ਸਹੀ ਤੌਰ 'ਤੇ ਪਛਾਣ ਸਕਦੇ ਹੋ ਕਿ ਕਿਹੜੇ ਆਬਜੈਕਟ ਖਤਮ ਹੋਣ ਵਿੱਚ ਅਸਫਲ ਰਹੇ।

ਇਹ ਕੋਈ ਅਮੂਰਤ ਸਿਧਾਂਤ ਨਹੀਂ ਹੈ। ਇੱਕ ਸਿੰਗਲ ਲੀਕ ਹੋਇਆ React component ਹਜ਼ਾਰਾਂ detached HTMLElement objects ਨੂੰ ਰੱਖ ਸਕਦਾ ਹੈ। ਉਹ ਆਬਜੈਕਟ ਹੁਣ ਦਿਖਾਈ ਦੇਣ ਵਾਲੇ document ਨਾਲ ਨਹੀਂ ਜੁੜੇ ਹੋਏ ਹਨ, ਪਰ JavaScript references ਉਹਨਾਂ ਨੂੰ collect ਹੋਣ ਤੋਂ ਰੋਕਦੇ ਹਨ। ਉਹ comparison view ਵਿੱਚ constructor ਦੇ ਨਾਮ Detached HTMLElement ਨਾਲ ਦਿਖਾਈ ਦਿੰਦੇ ਹਨ। ਜਦੋਂ ਤੁਸੀਂ ਉਹਨਾਂ ਨੂੰ ਵਧਦੇ ਹੋਏ ਦੇਖਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਨੂੰ ਆਪਣੀ ਲੀਕ ਮਿਲ ਗਈ ਹੈ।

Chrome DevTools ਵਰਕਫਲੋ (Workflow)

ਸਾਫ਼ ਸ਼ੁਰੂਆਤ ਕਰੋ। ਅਣਸੰਬੰਧਿਤ ਬ੍ਰਾਊਜ਼ਰ ਟੈਬਾਂ ਨੂੰ ਬੰਦ ਕਰੋ, ਅਣਸੰਬੰਧਿਤ extensions ਨੂੰ ਡਿਸੇਬਲ ਕਰੋ, ਅਤੇ ਆਪਣੀ ਐਪਲੀਕੇਸ਼ਨ ਨੂੰ ਇੱਕ ਸਥਿਰ ਅਵਸਥਾ (steady state) ਵਿੱਚ ਆਉਣ ਦਿਓ। Chrome DevTools ਖੋਲ੍ਹੋ, Memory tab 'ਤੇ ਜਾਓ, ਅਤੇ Heap snapshot ਚੁਣੋ। Take snapshot 'ਤੇ ਕਲਿੱਕ ਕਰੋ। ਇਹ baseline ਤੁਹਾਡੇ ਸ਼ੁਰੂਆਤੀ memory footprint ਨੂੰ ਕੈਪਚਰ ਕਰਦਾ ਹੈ।

ਹੁਣ ਉਹੀ ਉਪਭੋਗਤਾ ਐਕਸ਼ਨ (user action) ਕਰੋ ਜਿਸਦਾ ਤੁਹਾਨੂੰ ਸ਼ੱਕ ਹੈ। ਉਸ ਭਾਰੀ modal ਨੂੰ ਖੋਲ੍ਹੋ ਅਤੇ ਬੰਦ ਕਰੋ। widget ਨੂੰ mount ਅਤੇ unmount ਕਰੋ। route 'ਤੇ ਜਾਓ ਅਤੇ ਵਾਪਸ ਆਓ। ਇੱਕ ਵਾਰ ਜਦੋਂ UI ਆਪਣੀ ਅਸਲ ਵਿਜ਼ੂਅਲ ਅਵਸਥਾ ਵਿੱਚ ਵਾਪਸ ਆ ਜਾਂਦਾ ਹੈ, ਤਾਂ Memory tab ਵਿੱਚ ਕੂੜੇਦਾਨ (trash can) ਵਾਲੇ ਆਈਕਨ 'ਤੇ ਕਲਿੱਕ ਕਰੋ। ਇਹ ਇੱਕ global garbage collection pass ਨੂੰ ਲਾਗੂ ਕਰਦਾ ਹੈ। render cycle ਦੇ ਅਸਥਾਈ ਆਬਜੈਕਟ ਹਟ ਜਾਣੇ ਚਾਹੀਦੇ ਹਨ। ਜੋ ਕੁਝ ਵੀ ਬਚਦਾ ਹੈ, ਉਹ ਲੀਕ ਲਈ ਇੱਕ ਅਸਲ ਉਮੀਦਵਾਰ ਹੈ।

ਫਿਰ ਤੋਂ Take snapshot 'ਤੇ ਕਲਿੱਕ ਕਰੋ। ਹੁਣ ਤੁਹਾਡੇ ਕੋਲ ਮੈਮੋਰੀ ਦੀਆਂ ਦੋ ਤਸਵੀਰਾਂ ਹਨ। View ਨੂੰ Summary ਤੋਂ Comparison ਵਿੱਚ ਬਦਲੋ। Comparison scope ਨੂੰ ਪਹਿਲੇ snapshot 'ਤੇ ਸੈੱਟ ਕਰੋ। ਟੂਲ ਤੁਹਾਨੂੰ ਸਿਰਫ਼ ਉਹੀ ਦਿਖਾਏਗਾ ਜੋ ਦੋਵਾਂ ਕੈਪਚਰਾਂ ਦੇ ਵਿਚਕਾਰ ਬਦਲਿਆ ਹੈ, runtime ਦੇ ਫਾਲਤੂ ਸ਼ੋਰ (noise) ਨੂੰ ਹਟਾ ਕੇ।

Delta ਅਨੁਸਾਰ sort ਕਰੋ। ਉਹਨਾਂ object counts ਨੂੰ ਦੇਖੋ ਜੋ ਵਧੇ ਹਨ। Detached HTMLElement, Array, Function, ਜਾਂ ਆਪਣੇ ਕੋਡਬੇਸ ਤੋਂ ਨਾਮ ਵਾਲੇ class instances ਵਰਗੇ constructors 'ਤੇ ਖਾਸ ਧਿਆਨ ਦਿਓ। ਵਧਦਾ ਹੋਇਆ delta ਮਤਲਬ ਹੈ ਕਿ ਤੁਹਾਡੇ ਐਕਸ਼ਨ ਦੌਰਾਨ ਆਬਜੈਕਟ ਬਣਾਏ ਗਏ ਸਨ ਅਤੇ ਬਾਅਦ ਵਿੱਚ collect ਨਹੀਂ ਕੀਤੇ ਗਏ।

Retaining Path ਦਾ ਪਤਾ ਲਗਾਉਣਾ

ਜਦੋਂ ਤੁਸੀਂ ਲੀਕ ਹੋਇਆ element ਦੇਖਦੇ ਹੋ, ਤਾਂ ਉਸਨੂੰ ਚੁਣੋ। ਹੇਠਲਾ ਪੈਨਲ retaining path ਦਿਖਾਉਂਦਾ ਹੈ: ਰੈਫਰੈਂਸਾਂ ਦੀ ਇੱਕ ਲੜੀ ਜੋ ਦੱਸਦੀ ਹੈ ਕਿ ਇਹ ਆਬਜੈਕਟ ਅਜੇ ਵੀ ਕਿਉਂ ਜਿਉਂਦਾ ਹੈ। ਇਹ ਲੜੀ ਇੱਕ detached div ਤੋਂ ਸ਼ੁਰੂ ਹੋ ਕੇ React ਦੀਆਂ ਅੰਦਰੂਨੀ properties ਰਾਹੀਂ, ਇੱਕ closure ਵਿੱਚ ਜਾ ਸਕਦੀ ਹੈ, ਅਤੇ ਅੰਤ ਵਿੱਚ ਤੁਹਾਡੇ ਕੰਪੋਨੈਂਟਾਂ ਵਿੱਚੋਂ ਕਿਸੇ ਇੱਕ ਦੇ ਅੰਦਰ ਰਜਿਸਟਰ ਕੀਤੇ ਗਏ event listener 'ਤੇ ਰੁਕ ਸਕਦੀ ਹੈ। ਲੜੀ ਵਿੱਚ ਉਹ ਆਖਰੀ ਲਿੰਕ ਤੁਹਾਡੀ line number ਹੈ।

ਇੱਥੇ ਤੁਸੀਂ ਨਿਦਾਨ (diagnosis) ਤੋਂ ਮੂਲ ਕਾਰਨ (root cause) ਵੱਲ ਵਧਦੇ ਹੋ। ਜੇਕਰ retaining path window.addEventListener 'ਤੇ ਖਤਮ ਹੁੰਦਾ ਹੈ, ਤਾਂ ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ ਕਿ ਇੱਕ global listener ਤੁਹਾਡੇ ਕੰਪੋਨੈਂਟ ਨੂੰ ਕੈਦ ਕਰਕੇ ਰੱਖ ਰਿਹਾ ਹੈ। ਜੇਕਰ ਇਹ IntersectionObserver instance 'ਤੇ ਖਤਮ ਹੁੰਦਾ ਹੈ, ਤਾਂ ਤੁਸੀਂ ਜਾਣਦੇ ਹੋ ਕਿ ਇੱਕ observer ਅਜੇ ਵੀ ਉਸ node ਨੂੰ ਦੇਖ ਰਿਹਾ ਹੈ ਜਿਸ ਨੂੰ garbage collect ਹੋ ਜਾਣਾ ਚਾਹੀਦਾ ਸੀ।

React ਵਿੱਚ ਆਮ ਦੋਸ਼ੀ

React ਵਿੱਚ ਮੈਮੋਰੀ ਲੀਕ ਆਮ ਤੌਰ 'ਤੇ ਤਿੰਨ ਪੈਟਰਨਾਂ ਵਿੱਚ ਹੁੰਦੇ ਹਨ।

Orphaned global listeners. ਇੱਕ useEffect scroll position, key presses, ਜਾਂ resize events ਨੂੰ ਟ੍ਰੈਕ ਕਰਨ ਲਈ window ਜਾਂ document ਨਾਲ ਜੁੜਦਾ ਹੈ। ਜੇਕਰ effect ਇੱਕ cleanup function ਵਾਪਸ ਨਹੀਂ ਕਰਦਾ ਜੋ removeEventListener ਨੂੰ ਕਾਲ ਕਰਦਾ ਹੈ, ਤਾਂ listener ਪੇਜ ਦੇ ਜੀਵਨ ਕਾਲ (lifetime) ਤੱਕ ਜਿਉਂਦਾ ਰਹਿੰਦਾ ਹੈ। ਕਿਉਂਕਿ listener ਇੱਕ closure ਹੈ, ਇਹ React ਦੁਆਰਾ ਕੰਪੋਨੈਂਟ ਨੂੰ unmount ਕਰਨ ਦੇ ਲੰਬੇ ਸਮੇਂ ਬਾਅਦ ਵੀ ਪੂਰੇ component scope ਨੂੰ ਜਿਉਂਦਾ ਰੱਖਦਾ ਹੈ।

Uncleared observers. IntersectionObserver and ResizeObserver are powerful, but they create native references outside React’s control. If you instantiate an observer inside a component and forget to call disconnect() in the cleanup phase, the observer holds the target DOM node, and the DOM node holds React fibers, props, and state.

Closure traps. When you define a function inside a component and pass it to a third-party library, a global cache, or even setTimeout, that function closes over every variable in its lexical scope. If the external owner keeps the function around, it keeps your whole component scope around with it.

Cleanup Patterns That Actually Work

Fixing a leak means severing every retaining path you found in the snapshot.

Always return a cleanup function from useEffect. If you add a listener in the effect, remove it there.

Use useCallback for any handler you attach to the DOM or window. Without it, every render creates a new function reference. If you call addEventListener with one reference and later call removeEventListener with a different one, the removal fails silently. The original listener stays on window forever. useCallback keeps the reference stable so that add and remove match exactly.

Handle observers with the same discipline. Store the observer instance in a ref or local variable inside the effect. In the cleanup function, call observer.disconnect(). Do not assume unmounting the component kills the observer. It does not.

If your component publishes anything to a global namespace or a singleton service, delete those references on unmount. The V8 engine can only reclaim memory when an object is truly unreachable. Leaving a hook on window or an entry in a module-level Map creates an invisible bridge that keeps the heap growing.

The Real Takeaway

Memory leaks do not crash your app immediately. They accumulate one detached node at a time during long user sessions. The fix is not a library upgrade or a compiler flag. It is the habit of proving your cleanup logic with heap snapshots.

Take a baseline, trigger the suspect flow, force garbage collection, and compare. If the delta shows growth, inspect the retaining path, find the listener or observer that should not exist, and cut the reference. Run the test again. When the delta stays flat, you have actually solved it. Your application will stay responsive, and your users will not lose their work to a frozen browser tab.