ನಿಮ್ಮ ಬಳಕೆದಾರರ ಬ್ರೌಸರ್ ಟ್ಯಾಬ್ ಮೂವತ್ತು ನಿಮಿಷಗಳ ನಂತರ ಫ್ರೀಜ್ ಆಗುತ್ತದೆ. UI ಅಡಚಣೆಗೊಳ್ಳುತ್ತದೆ. ನಂತರ ಬ್ರೌಸರ್ 'out-of-memory' ದೋಷದೊಂದಿಗೆ ಕ್ರ್ಯಾಶ್ ಆಗುತ್ತದೆ.

ನೀವು ಕಂಪೊನೆಂಟ್ ಕೋಡ್ ಅನ್ನು ಸಾಲು ಸಾಲಾಗಿ ಪರಿಶೀಲಿಸುತ್ತೀರಿ ಮತ್ತು ಅದರಲ್ಲಿ ಏನೂ ತಪ್ಪಿಲ್ಲ ಎಂದು ಕಾಣುತ್ತದೆ. React ನಲ್ಲಿ ಮೆಮೊರಿ ಲೀಕ್‌ಗಳ (memory leaks) ಅತ್ಯಂತ ಕಿರಿಕಿರಿ ಉಂಟುಮಾಡುವ ಭಾಗವಿದು. ಈ ಬಗ್ ನಿಮ್ಮ JSX ಸಿಂಟ್ಯಾಕ್ಸ್ ಅಥವಾ ನಿಮ್ಮ hook ಲಾಜಿಕ್‌ನಲ್ಲಿ ಇರುವುದಿಲ್ಲ. ಇದು ನಿಮ್ಮ ಕಂಪೊನೆಂಟ್ ಮತ್ತು ಬ್ರೌಸರ್‌ನ 'garbage collector' ನಡುವಿನ ಅಂತರದಲ್ಲಿ ಅಡಗಿರುತ್ತದೆ. ಒಂದು ಅಲೆಯುವ (stray) event listener ಅಥವಾ ದೀರ್ಘಕಾಲದ closure ಮೆಮೊರಿಯನ್ನು ಮರುಪಡೆಯುವಿಕೆಯಿಂದ (reclaiming) ಕಲೆಕ್ಟರ್ ಅನ್ನು ತಡೆಯುತ್ತದೆ. ನೀವು ಒಂದು ಕಂಪೊನೆಂಟ್ ಅನ್ನು unmount ಮಾಡುತ್ತೀರಿ, ಆದರೆ ಒಂದು ಉಳಿದಿರುವ ರೆಫರೆನ್ಸ್ (lingering reference) ಇಡೀ ಟ್ರೀ ಅನ್ನು heap ನಲ್ಲಿ ಜೀವಂತವಾಗಿರಿಸುತ್ತದೆ. ಕೇವಲ ಕೋಡ್ ಓದುವುದರಿಂದ ನೀವು ಈ ಲೀಕ್‌ಗಳನ್ನು ಪತ್ತೆಹಚ್ಚಲು ಸಾಧ್ಯವಿಲ್ಲ. ಸಮಸ್ಯೆ ಮೇಲ್ಮೈ ಅಡಿಯಲ್ಲಿ ಅಡಗಿರುತ್ತದೆ, ಕಣ್ಣಿಗೆ ಕಾಣಿಸುವುದಿಲ್ಲ ಮತ್ತು ಯೂನಿಟ್ ಟೆಸ್ಟ್‌ಗಳಲ್ಲಿ (unit tests) ಸುಪ್ತವಾಗಿರುತ್ತದೆ.

ಲೀಕ್‌ಗಳು ಕಣ್ಣೆದುರೇ ಏಕೆ ಅಡಗಿರುತ್ತವೆ

V8 ನಂತಹ JavaScript ಇಂಜಿನ್‌ಗಳು ಮೆಮೊರಿಯನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನಿರ್ವಹಿಸುತ್ತವೆ. ಒಂದು ಆಬ್ಜೆಕ್ಟ್‌ಗೆ root ನಿಂದ ಯಾವುದೇ ರೆಫರೆನ್ಸ್ ಪಾತ್ ಇಲ್ಲದಿದ್ದಾಗ, ಇಂಜಿನ್ ಆ ಆಬ್ಜೆಕ್ಟ್ ಅನ್ನು 'garbage' ಎಂದು ಗುರುತಿಸುತ್ತದೆ ಮತ್ತು ಆ ಜಾಗವನ್ನು ಮರುಪಡೆಯುತ್ತದೆ. ನೀವು ಉದ್ದೇಶಿಸಿದതിಗಿಂತ ಹೆಚ್ಚು ಕಾಲ ಒಂದು ಗುಪ್ತ ರೆಫರೆನ್ಸ್ ಉಳಿದಿರುವವರೆಗೆ ಈ ಪ್ರಕ್ರಿಯೆಯು ಚೆನ್ನಾಗಿ ಕೆಲಸ ಮಾಡುತ್ತದೆ.

React ನಲ್ಲಿ, ಅಪಾಯವು ಹೆಚ್ಚಾಗಿ ಕಂಪೊನೆಂಟ್‌ಗಳು ಮತ್ತು DOM ನಡುವಿನ ಗಡಿಯಲ್ಲಿ ಕಾಣಿಸಿಕೊಳ್ಳುತ್ತದೆ. ನೀವು ಒಂದು modal ಒಳಗೆ window ಗೆ resize listener ಅನ್ನು ಅtಚ್ ಮಾಡಬಹುದು ಅಥವಾ dashboard widget ನಲ್ಲಿ WebSocket ಗೆ ಸಬ್‌ಸ್ಕ್ರೈಬ್ ಮಾಡಬಹುದು. ಬಳಕೆದಾರರು modal ಅನ್ನು ಮುಚ್ಚಿದಾಗ ಅಥವಾ ಬೇರೆ ಪುಟಕ್ಕೆ ಹೋದಾಗ, ಕಂಪೊನೆಂಟ್ unmount ಆಗುತ್ತದೆ. ಒಂದು ವೇಳೆ ಸಬ್‌ಸ್ಕ್ರಿಪ್ಷನ್ ಉಳಿದಿದ್ದರೆ, ಇಂಜಿನ್ ಗ್ಲೋಬಲ್ window ಆಬ್ಜೆಕ್ಟ್‌ನಿಂದ ನಿಮ್ಮ handler ವರೆಗೆ ಮತ್ತು ನಿಮ್ಮ handler ನಿಂದ ಕಂಪೊನೆಂಟ್‌ನ closure ವರೆಗೆ ಒಂದು ಮಾನ್ಯವಾದ ರೆಫರೆನ್ಸ್ ಅನ್ನು ಕಾಣುತ್ತದೆ. ಕಂಪೊನೆಂಟ್, ಅದರ props, ಅದರ state ಮತ್ತು ಅದರ ಇಡೀ DOM nodes subtree ಮೆಮೊರಿಯಲ್ಲಿ ಸ್ಥಿರವಾಗಿ (pinned) ಉಳಿಯುತ್ತವೆ. ನೂರಾರು ಇಂಟರಾಕ್ಷನ್‌ಗಳ ನಂತರ, ಈ ಸ್ಥಿರವಾದ ಆಬ್ಜೆಕ್ಟ್‌ಗಳು ಸಂಗ್ರಹವಾಗುತ್ತವೆ. ಮೆಮೊರಿ ಬಳಕೆ 'sawtooth pattern' ನಲ್ಲಿ ಏರಿಳಿತಗೊಳ್ಳುತ್ತಾ ಹೋಗುತ್ತದೆ ಮತ್ತು ಎಂದಿಗೂ ಸಂಪೂರ್ಣವಾಗಿ ಕೆಳಕ್ಕೆ ಇಳಿಯುವುದಿಲ್ಲ.

ಎಂಟರ್‌ಪ್ರೈಸ್ ಡ್ಯಾಶ್‌ಬೋರ್ಡ್ ಸಮಸ್ಯೆ (The Enterprise Dashboard Problem)

ಬಳಕೆದಾರರು ಗಂಟೆಗಟ್ಟಲೆ ಒಂದೇ ಪುಟದಲ್ಲಿ ಇರುತ್ತಾರೆ ಎನ್ನುವ ಎಂಟರ್‌ಪ್ರೈಸ್ ಡ್ಯಾಶ್‌ಬೋರ್ಡ್‌ಗಳಲ್ಲಿ ಇದು ಹೆಚ್ಚಾಗಿ ಸಂಭವಿಸುತ್ತದೆ. ಮಾನಿಟರಿಂಗ್ ಪ್ಯಾನಲ್‌ಗಳು, ಅನಾಲಿಟಿಕ್ಸ್ ವ್ಯೂಗಳು ಅಥವಾ ಟಿಕೆಟಿಂಗ್ ಸಿಸ್ಟಮ್‌ಗಳನ್ನು ನೆನಪಿಸಿಕೊಳ್ಳಿ. ಬಳಕೆದಾರರು ಒಂದು detail modal ಅನ್ನು ತೆರೆಯುತ್ತಾರೆ, ದೊಡ್ಡ ಡೇಟಾಸೆಟ್ ಅನ್ನು ಫಿಲ್ಟರ್ ಮಾಡುತ್ತಾರೆ ಅಥವಾ single-page app ನಲ್ಲಿ ಟ್ಯಾಬ್‌ಗಳನ್ನು ಬದಲಾಯಿಸುತ್ತಾರೆ. ಪ್ರತಿಯೊಂದು ವೈಯಕ್ತಿಕ ಇಂಟರಾಕ್ಷನ್ ಕೂಡ ಸರಿಯಾಗಿರುವಂತೆ ಭಾಸವಾಗುತ್ತದೆ. ಆದರೆ ಕಾಲಾನಂತರದಲ್ಲಿ, ಅನಾಥವಾದ (orphaned) nodes ಮತ್ತು ಡಿಟ್ಯಾಚ್ ಆದ (detached) listeners ಸಂಗ್ರಹವಾಗುತ್ತವೆ. ಅಪ್ಲಿಕೇಶನ್ ನಿಧಾನವಾಗುವುದು ಕೇವಲ ಒಂದು ದುಬಾರಿ (expensive) render ನಿಂದಲ್ಲ, ಬದಲಾಗಿ heap ಎಷ್ಟು ದೊಡ್ಡದಾಗುತ್ತದೆ ಎಂದರೆ ಅದು ಪದೇ ಪದೇ ಮತ್ತು ದುಬಾರಿ garbage collection pauses ಅನ್ನು ಉಂಟುಮಾಡುತ್ತದೆ.

ನಿಮ್ಮ useEffect hooks ಬಗ್ಗೆ ಕೇವಲ ಊಹಿಸುವುದನ್ನು ನಿಲ್ಲಿಸಿ. ಲೀಕ್ ಇದೆಯೇ ಎಂದು ತಿಳಿಯಲು ಏಕೈಕ ಮಾರ್ಗವೆಂದರೆ heap ಅನ್ನು ನೇರವಾಗಿ ಅಳೆಯುವುದು. Chrome DevTools ನಿಮಗೆ ಆ ದೃಶ್ಯತೆಯನ್ನು (visibility) ನೀಡುತ್ತದೆ.

Chrome DevTools ಬಳಸಿ ಲೀಕ್‌ಗಳನ್ನು ಪತ್ತೆಹಚ್ಚುವುದು

ನಿಮಗೆ ಪುನರಾವರ್ತಿಸಬಹುದಾದ (reproducible) ಸರಣಿ ಮತ್ತು ಕೆಲವು ನಿಮಿಷಗಳ ಏಕಾಗ್ರತೆಯ ಅಗತ್ಯವಿದೆ. ನಿಮ್ಮ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು Chrome ನಲ್ಲಿ ತೆರೆಯಿರಿ, DevTools ಪ್ರಾರಂಭಿಸಿ ಮತ್ತು Memory ಟ್ಯಾಬ್‌ಗೆ ಹೋಗಿ.

ಬೇಸ್‌ಲೈನ್ ರೆಕಾರ್ಡ್ ಮಾಡಿ (Record a baseline). Heap snapshot ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ ಮತ್ತು Take snapshot ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ. ಇದು ಪ್ರಸ್ತುತ JavaScript heap ನಲ್ಲಿರುವ ಪ್ರತಿಯೊಂದು ಆಬ್ಜೆಕ್ಟ್ ಅನ್ನು ಸೆರೆಹಿಡಿಯುತ್ತದೆ ಮತ್ತು ಪ್ರಾರಂಭಿಕ ಮೆಮೊರಿಯನ್ನು ತೋರಿಸುತ್ತದೆ. ಇದನ್ನು ಪುಟವು ತನ್ನ ಆರಂಭಿಕ idle ಸ್ಥಿತಿಗೆ ಬಂದ ನಂತರ ಮಾಡಿ, ಆರಂಭಿಕ ಲೋಡ್ ಸಮಯದಲ್ಲಿ ಅಲ್ಲ, ಇದರಿಂದ ನೀವು ಬಳಕೆದಾರರ ಕ್ರಿಯೆಗಳಿಂದ ಉಂಟಾದ ಬೆಳವಣಿಗೆಯನ್ನು ಮಾತ್ರ ಅಳೆಯಬಹುದು.

ಕ್ರಿಯೆಯನ್ನು ಪ್ರಚೋದಿಸಿ (Trigger the action). ಲೀಕ್ ಉಂಟುಮಾಡುತ್ತದೆ ಎಂದು ನೀವು ಶಂಕಿಸುವ ನಿಖರವಾದ UI ಇಂಟರಾಕ್ಷನ್ ಅನ್ನು ಮಾಡಿ. ಒಂದು modal ಅನ್ನು ತೆರೆಯಿರಿ ಮತ್ತು ಮುಚ್ಚಿ, ಒಂದು ಸಂಕೀರ್ಣವಾದ ಚಾರ್ಟ್ ಅನ್ನು ಟೋಗಲ್ ಮಾಡಿ ಅಥವಾ ಒಂದು route ಅನ್ನು ಬದಲಾಯಿಸಿ ಮತ್ತೆ ಹಿಂದಕ್ಕೆ ಬನ್ನಿ. ಮುಗಿದ ನಂತರ, ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಅದರ ಮೂಲ ದೃಶ್ಯ ಸ್ಥಿತಿಗೆ (original visual state) ತನ್ನಿ. ಈ ಹಂತವು ಬಹಳ ಮುಖ್ಯವಾಗಿದೆ. ಬೇಸ್‌ಲೈನ್ ಸಮಯದಲ್ಲಿ UI ಹೇಗಿತ್ತೋ ಹಾಗೆಯೇ ಕಾಣಿಸಿಕೊಳ್ಳಬೇಕೆಂದು ನೀವು ಬಯಸುತ್ತೀರಿ. UI ಖಾಲಿ ಇರುವಂತೆ ಕಂಡರೂ heap ಬೆಳೆದಿದ್ದರೆ, ನಿಮ್ಮ ಬಳಿ ಲೀಕ್ ಆಗಿರುವ ಬಗ್ಗೆ ಬಲವಾದ ಪುರಾವೆ ಇದೆ ಎಂದರ್ಥ.

Garbage collection ಅನ್ನು ಬಲವಂತವಾಗಿ ಮಾಡಿ (Force garbage collection). Memory ಟ್ಯಾಬ್‌ನಲ್ಲಿರುವ ಕಸದ ಬುಟ್ಟಿ (trash can) ಐಕಾನ್ ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ. ಇದು ಪೂರ್ಣ GC ಸೈಕಲ್ ಅನ್ನು ಪ್ರಚೋದಿಸುತ್ತದೆ ಮತ್ತು ಮುಂದಿನ ಸಂಗ್ರಹಣೆಯವರೆಗೆ ಕಾನೂನುಬದ್ಧವಾಗಿ ಉಳಿದಿರುವ ತಾತ್ಕಾಲಿಕ ಆಬ್ಜೆಕ್ಟ್‌ಗಳನ್ನು ತೆರವುಗೊಳಿಸುತ್ತದೆ. ಉಳಿದುಕೊಂಡಿರುವುದು ನಿಜವಾದ ಲೀಕ್‌ಗಳು, ಅಂದರೆ ಸಂಗ್ರಹಿಸಲ್ಪಡಬೇಕಿದ್ದ ಆದರೆ ಅಕಸ್ಮಾತ್ ರೆಫರೆನ್‌ಸ್‌ಗಳಿಂದ ಜೀವಂತವಾಗಿ ಉಳಿದಿರುವ ಆಬ್ಜೆಕ್ಟ್‌ಗಳು.

ಎರಡನೇ snapshot ತೆಗೆದುಕೊಳ್ಳಿ. ಮತ್ತೆ Take snapshot ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ. ಈಗ ನಿಮ್ಮ ಬಳಿ ಒಂದೇ ರೀತಿಯ UI ಪರಿಸ್ಥಿತಿಗಳಲ್ಲಿ ತೆಗೆದ heap ನ ಎರಡು ಫೋಟೋಗಳಿವೆ.

ಫಲಿತಾಂಶಗಳನ್ನು ಹೋಲಿಸಿ (Compare results). Summary ದಿಂದ Comparison ಗೆ ವ್ಯೂ ಅನ್ನು ಬದಲಾಯಿಸಿ. ಮೊದಲ snapshot ಅನ್ನು baseline ಆಗಿ ಮತ್ತು ಎರಡನೇ snapshot ಅನ್ನು compared snapshot ಆಗಿ ಹೊಂದಿಸಿ. Comparison ವ್ಯೂ ಪ್ರತಿಯೊಂದು ಆಬ್ಜೆಕ್ಟ್ ವರ್ಗವನ್ನು ಪಟ್ಟಿ ಮಾಡುತ್ತದೆ ಮತ್ತು delta ಅನ್ನು ತೋರಿಸುತ್ತದೆ, ಅಂದರೆ ಎರಡು ಕ್ಯಾಪ್ಚರ್‌ಗಳ ನಡುವಿನ ಆಬ್ಜೆಕ್ಟ್ ಸಂಖ್ಯೆಗಳಲ್ಲಿನ ನಿವ್ವಳ ಬದಲಾವಣೆ.

Delta ಮೂಲಕ ವಿಂಗಡಿಸಿ (Sort by Delta). ಗಮನಾರ್ಹವಾಗಿ ಹೆಚ್ಚಾದ ವರ್ಗಗಳನ್ನು ಹುಡುಕಿ. ವಿಶೇಷವಾಗಿ Detached HTMLElement ಮತ್ತು React fiber nodes ಮೇಲೆ ಗಮನ ಹರಿಸಿ. Detached HTML ಎಲಿಮೆಂಟ್ ಎಂದರೆ ಸಕ್ರಿಯ ಡಾಕ್ಯುಮೆಂಟ್ ಟ್ರೀಗೆ ಇನ್ನು ಅtಚ್ ಆಗದಿದ್ದರೂ, ಯಾವುದೋ ಒಂದು JavaScript ರೆಫರೆನ್ಸ್ ಅದನ್ನು ಹಿಡಿದಿಟ್ಟುಕೊಂಡಿರುವ DOM ನೋಡ್ ಆಗಿದೆ. ಇವು ಸ್ಪಷ್ಟವಾದ ಸಾಕ್ಷ್ಯಗಳು (smoking guns). ನೀವು modal ಅನ್ನು ಮುಚ್ಚಿದ ನಂತರ ಅಥವಾ ಕಂಪೊನೆಂಟ್ ಅನ್ನು unmount ಮಾಡಿದ ನಂತರ ಇವು ಇರಬಾರದು.

Retaining Path ಅನ್ನು ಓದುವುದು

When you select a detached element in the snapshot, Chrome shows the retaining path in the bottom panel. This path is a chain of references from the root down to the selected object. Follow it carefully. You will often find an event listener, an IntersectionObserver, a setInterval ID, or a closure pointing to a specific line in your component.

Look for names you recognize. If you see a listener attached to window with a function name from your codebase, you have found the anchor. The object holding that listener is keeping your entire component alive. Sometimes the chain runs through a third-party library. In those cases, check whether the library expects an explicit teardown call that you forgot to invoke in a cleanup function.

Fixing the Root Causes

Once you identify the retaining path, the fix is usually mechanical but requires discipline across the team.

Use cleanup functions. Always return a cleanup function in useEffect when you add listeners to the window or document. If your effect subscribes to a resize event, remove that subscription before the component unmounts. The cleanup runs when React tears down the component, giving you a guaranteed hook to sever external connections.

Stabilize references. Wrap your handlers in useCallback. This ensures you pass the exact same function reference to removeEventListener that you originally passed to addEventListener. If you register an inline function like window.addEventListener('resize', () => { ... }) and later try to remove it with another inline function, the references will not match. The listener stays attached. The closure inside it keeps your component state alive. useCallback with a stable dependency array prevents this identity mismatch.

Sever global links. Remember that the browser's event target objects, like window and document, live for the lifetime of the page. Any reference from them into your component acts as a global anchor. Removing the listener breaks that anchor and lets the V8 engine sweep away the component state and DOM nodes during the next garbage collection cycle.

Consider a modal that tracks window width. Without cleanup, each time the user opens the modal, a new listener attaches. The old ones never detach because the component instances they belong to are gone, yet the functions themselves were anonymous and lost. Using a named handler wrapped in useCallback, plus a cleanup function that calls removeEventListener, closes the loop cleanly.

A Real Takeaway

Memory leaks in React rarely announce themselves with a clear error message. They announce themselves with a tab that grows heavier the longer it stays open. Do not waste time speculating about which hook is guilty. Open the Memory tab, force garbage collection, and compare snapshots. Let the heap profiler show you the exact retaining path. Then write the cleanup function, stabilize the callback reference, and cut the global link. Your users will not notice the fix directly, but they will notice that the dashboard still runs smoothly at the end of the day.