LogoTRUONG PHAM
Home
Projects
Blogs
YouTube
Contact

Newsletter

Stay updated with technical artifacts and engineering insights.

LogoTRUONG PHAM

Building scalable software and sharing insights on technology & life.

Sitemap

  • Home
  • Projects
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back
Blog #8: Memory Trash – The price of forgetting to 'clean up' your Event Listeners
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

Blog #8: Memory Trash – The price of forgetting to 'clean up' your Event Listeners

The story of an app that crashed after 30 minutes of use, and the journey to track down the silent killer known as Memory Leak.

TP
Truong PhamSoftware Engineer
PublishedApril 1, 2024

I once worked on a project building a web-based Drawing Tool. Users could drag and drop shapes, resize them, and draw freely. Everything was great until the QA department sent me a report: "The app runs very smoothly at first, but after about 30 minutes of continuous dragging and dropping, the browser starts to lag and finally... crashes (Out of Memory)."

I opened Chrome Task Manager and witnessed a horrifying sight: my app tab was consuming 4GB of RAM and the number continued to increase every time I clicked the mouse.

I knew I had just encountered the scariest "monster" in JavaScript programming: the Memory Leak.

1. The Silent Killer: Orphaned References

In JavaScript, the Garbage Collector is very smart; it will delete objects that are no longer in use. But it only does so when that object no longer has any links (references).

When you write window.addEventListener('mousemove', handleMove), you've just created a permanent link between the global window object and the handleMove function inside your component. Even when that component is removed from the screen (unmount), window still holds that function. And because that function is inside the component, it holds all variables, state, and child components... Result? The entire old component tree still sits in memory, unable to be deleted.

2. The Problem: A crack from a drag-and-drop effect

In my drawing tool, every time a user selects a shape, I listen for mouse move events to update the position.

// My elementary mistake
useEffect(() => {
  const handleMouseMove = (e) => {
    // Update position logic...
    console.log('Moving...');
  };

  window.addEventListener('mousemove', handleMouseMove);
  
  // Forgot: return () => window.removeEventListener('mousemove', handleMouseMove);
}, [selectedId]); 

Every time the user selects a new shape (selectedId changes), a new Listener is added. After 100 selections, there are 100 handleMouseMove functions running in the background simultaneously. Each function holds a bunch of old data. My computer started heating up, the fan got loud, and boom—Blue Screen.

3. The Process: Using the Memory Lab "microscope"

To debug this error, I couldn't use console.log. I had to use the Memory tool in Chrome DevTools.

  1. I took a Heap Snapshot when the app just opened.
  2. Performed the shape selection 50 times.
  3. Took a second snapshot.
  4. Used the Comparison feature to see what was created and not deleted.

The result was a long list of Detached HTMLElements. Those are the fingerprints of a Memory Leak. I realized hundreds of event handlers were still surviving even though the components had died long ago.

The final solution: Always, always have a cleanup function.

useEffect(() => {
  const handleMouseMove = (e) => { ... };
  window.addEventListener('mousemove', handleMouseMove);

  return () => {
    // Clean up before leaving
    window.removeEventListener('mousemove', handleMouseMove);
  };
}, [selectedId]);

4. Trade-offs: More code or more Trash?

Many will say: "Writing that removeEventListener is so tiring, why not use { once: true }?". It's true that { once: true } is very convenient, but it's only for events that run once (like clicking to open a menu). For continuous events like scroll, mousemove, or resize, manual cleanup is the only way.

The price to pay is that the code looks a bit longer and you have to always be alert. If you forget, you don't just break your app, but you also ruin the user's computer.

5. Junior vs. Senior: Resource Awareness

A Junior developer often only cares about the business logic: "How to get the shape to move with the mouse?". Once it runs, you consider the task complete.

A Senior developer spends 30% of their time thinking "How to clean up?". They understand that Frontend is a long-running process. A user might keep an app tab open all day. If every action they take leaks even just 1KB, then after 8 hours of work, that number becomes huge.

Sustainable solutions:

  • Build Custom Hooks to manage events (e.g., useEventListener). Encapsulating cleanup logic in one place ensures you never forget.
  • Use the Chrome Performance Monitor to track RAM charts periodically.
  • In large projects, try using WeakMap or WeakSet to store data attached to the DOM, helping trash get automatically cleaned when the DOM is deleted.

6. Lessons Learned

Source code is like a party. You can set out many delicious dishes (features), but if the party is over and you don't clean up, the house will become smelly and unlivable.

  • Every addEventListener must have a corresponding removeEventListener.
  • Every setInterval or setTimeout must be cleared when unmounting.
  • Every WebSocket connection or Subscription must be closed.

Final advice: Become a "clean" developer. Don't let users have to restart their browser just because of one sloppy line of code.


Professional Frontend is when you leave a component and leave no trace in memory.

Series • Part 8 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #9: Safari – The new 'Internet Explorer' of the modern era?
Blog #7: The 'Double' Nightmare – When React StrictMode tests your patience
03Blog #3: Infinite Loops – When useEffect turns the browser into a 'furnace'04Blog #4: Hydration Error – When SSR's perfection 'slaps' me in the face at midnight05Blog #5: 200 OK – The system's sweet little lies06Blog #6: Why you shouldn't fully trust console.log – The silent deceiver07Blog #7: The 'Double' Nightmare – When React StrictMode tests your patience08Blog #8: Memory Trash – The price of forgetting to 'clean up' your Event ListenersReading09Blog #9: Safari – The new 'Internet Explorer' of the modern era?10Blog #10: Specificity War – When !important marks the start of a civil war11Blog #11: Don't wait until 'heavy web' to lose weight for your Bundle12Blog #12: Don't bring the whole supermarket home just to buy a loaf of bread13Blog #13: When optimization becomes a burden – Don't 'memo' everything you see
TP

Written by Truong Pham

Software Engineer passionate about building high-performance systems and meaningful experiences.

Read more articles