Timers and event listeners
1. What timers and event listeners do
setTimeout,setInterval,addEventListenercreate asynchronous tasks that live separately from the main JS execution stack.- These tasks hold references to everything used inside their callbacks (via closures).
While a timer is active or a listener is still "subscribed", every object its callback touches is considered reachable → the GC cannot remove it.
2. Why cleanup matters
Without cleanup, objects stay in memory forever
javascript
function startTimer() {
const bigData = new Array(1e6).fill('data');
setInterval(() => console.log(bigData.length), 1000);
}
startTimer();Here bigData is never freed:
- the interval lives forever;
- the closure holds a reference to
bigData; - the GC considers
bigData"alive" → memory grows.
You need to clean up:
javascript
const id = setInterval(...);
clearInterval(id);Event listeners are the same story
javascript
const button = document.getElementById('btn');
button.addEventListener('click', () => console.log('clicked'));
button.remove(); // the element is removed, the listener remainsNow:
- the
buttonelement is removed from the DOM, but the callback is still kept in memory (JS still has a reference to it); - the GC cannot remove the object because a registered handler is "looking" at it.
Solution:
javascript
button.removeEventListener('click', handler);3. Why this is especially critical in SPAs
In an SPA the page does not reload. Anything that isn't cleaned up lives until the user closes the tab.
For example:
- you navigate between pages;
- components mount and unmount;
- if timers and listeners are not removed, each "old" component keeps existing in memory → garbage builds up.
After 30 minutes of using a React app you might see:
- hundreds of active intervals,
- dozens of "zombie" listeners,
- heap memory growing without being reclaimed.
4. How exactly this holds memory (the mechanism)
| Object | Reference | Why it isn't cleaned up |
|---|---|---|
| Timer | the callback (closure) | holds references to all closure variables |
| Listener | callback + DOM element | live until removeEventListener is called |
| WebSocket, RxJS, setInterval | an active subscription | holds the whole context |
| React useEffect | without cleanup | effects are never canceled, references persist |
The GC considers all of this reachable, because there are active callback references.
5. How to clean up correctly
Timers
javascript
useEffect(() => {
const id = setInterval(() => {...}, 1000);
return () => clearInterval(id);
}, []);Event listeners
javascript
useEffect(() => {
const handler = () => console.log('scroll');
window.addEventListener('scroll', handler);
return () => window.removeEventListener('scroll', handler);
}, []);Subscriptions (RxJS, WebSocket, custom events)
javascript
useEffect(() => {
const sub = stream$.subscribe(...);
return () => sub.unsubscribe();
}, []);6. What happens if you don't clean up
| Consequence | What happens |
|---|---|
| Memory growth | callback closures hold large objects |
| UI lag | the event loop is clogged with active callbacks |
| Duplicate calls | old listeners fire alongside new ones |
| CPU growth | too many active intervals |
| "Out of memory" | the browser or tab crashes |
| "Zombie components" | old React components keep living and running effects |
7. Tools for detecting this
- Chrome DevTools → Memory → Heap snapshot
→ look for "Detached DOM nodes" and
Timeout,EventListenerobjects. - Performance → Record → "Long tasks".
- React DevTools Profiler → check that components unmount.
- Sentry Performance → heap growth, leaks, FPS degradation.
8. Summary
| What to remember | Why |
|---|---|
setTimeout / setInterval must be cleared | otherwise the callback stays alive |
addEventListener must be removed | otherwise the DOM node and function are not freed |
In React, always return () => cleanup() in useEffect | prevents "zombie" effects |
| SPAs have no memory "restart" | everything accumulates until you clean it up manually |
| Memory leaks = lag + crashes | the GC cannot remove "alive" references |
The main idea:
A timer or a listener is like an open tab in memory. If you don't close it, you end up with hundreds of them, and the browser "dies".
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.