Web Development

My Journey Debugging React Performance: From "Laggy" to "Instant"

January 18, 20268 min read

You know that feeling when you think you've built the perfect component? Clean code, extracted hooks, everything modular... and then you open it on a mobile device and it feels like you're dragging the cursor through molasses. Yeah, that was me last Tuesday.

Chrome Performance tab showing significant scripting delay

Image 1: The "Yellow Wall of Doom" - Main thread blocked for 400ms+.

The "It Works on My Machine" Syndrome

I was working on the new Titration Simulator for the lab (coming soon, by the way!). On my desktop with its 32GB of RAM, the chart re-rendered instantly. But when I asked my colleague to try it on their older laptop, they just frowned. "Is it supposed to freeze when I drag the slider?" they asked.

Ouch.

So, I popped open the Chrome DevTools Performance tab. If you haven't used this, it's intimidating at first, but honestly, it's the only way to see what's really happening. I hit "Record," dragged the slider for 3 seconds, and hit "Stop."

The result? A solid block of yellow. The main thread was blocked for 400ms per frame. For context, smoother than butter requires 16ms (60fps). I was hitting 2fps.

The Suspect: Over-Optimization?

Ironically, I thought I was being smart. I had wrapped everything in `useMemo` and `useCallback`. I figured, "Hey, if I memoize it, it won't re-calculate, right?"

Well, here's the thing about `useMemo`: it has a cost. React has to compare the dependency array every singly render.

VS Code showing React component with maximum update depth error

Image 2: The dreaded "Maximum update depth exceeded" error.

I wasn't getting that error exactly, but I was creating a dependency loop. My `useMemo` depended on an object that was being recreated on every render because... wait for it... I forgot to `useMemo` the parent object!

The Fix: Simplicity Wins

I stripped it all out. I removed every single `useMemo` that wasn't strictly necessary. I stopped passing full objects as props and started passing only the primitives (strings, numbers) that the child component actually needed.

The code looked "dumber," but it ran 10x faster.

Key Takeaways for Lab Tools

Building tools for science means precision is key, but so is usability. If a researcher is trying to calculate a molarity in a rush, they don't care how "clean" my code architecture is. They care if the button clicks now.

  • Measure first, optimize second. Don't guess where the bottleneck is.
  • Primitives are your friends. Passing `{ value: 1 }` as a prop is a new object every time. Passing `1` is just `1`.
  • Test on low-end devices. If it runs on a Chromebook, it'll fly on a MacBook.
Clean performance timeline showing 60fps

Image 3: Smooth sailing – 60fps and plenty of idle time.

Now, back to coding the next calculator. I promise this one will be fast.

Share this article