LogoTRUONG PHAM
Home
Projects
Portfolio
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
  • Portfolio
  • Blogs
  • YouTube
  • Contact

Connect

  • GitHub
  • LinkedIn
  • Email
  • YouTube

© 2024 TRUONG PHAM. © All rights reserved.

Privacy PolicyTerms of Service
Back/50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES
Blog #16: Misplaced Debounce – When a localized solution becomes a system burden

Blog #16: Misplaced Debounce – When a localized solution becomes a system burden

A story of a laggy Search input and a lesson on where to place processing logic in the system's data flow.

April 22, 2024·3 min read

I received a bug report from a user: "When I type quickly into the search box, the characters appear very slowly, and sometimes the order jumps around".

A developer's first reaction is usually to look at the SearchInput component. I saw a debounce function placed right there. The logic seemed fine. But when looking closely at the System Flow, I realized the problem wasn't in that input box. it was about how data "flowed" through the application layers.

1. The problem is not with a single Component

When a user types, a chain of events is triggered. In complex systems, the "input box" doesn't just change text. It triggers global filters, changes URL params, and sends signals to neighboring charts.

Our problem was: We placed the debounce at the UI (Component) layer, but let the State Flow run too fast at the parent layer. With every keystroke, even though the search result was delayed (debounced), the entire parent component tree still re-rendered to update the "typing state."

2. Analysis of Data & State Flow

Look at how data moves:

  • Data Flow: User Input → Local State → Global Store → Service API.
  • State Flow: Every keystroke → Update Store → Trigger 10 Child Subscribers → Re-render 100 components.

If we debounce at the Input, we're only delaying the API call. But those 100 components still have to wake up just to know that "the user just typed another letter."

// Common error: Localized Debounce (Quick Fix)
const SearchBox = ({ onChange }) => {
  const debouncedChange = debounce(onChange, 300);
  
  return <input onChange={(e) => {
    // UI Local state still updates with every key
    // But onChange (system impact) is slowed down
    debouncedChange(e.target.value);
  }} />;
};

3. Local Fix vs. Architectural Refactoring

Local Fix (Quick Fix): Add memo to child components to reduce their need to re-render. This is "patching where it hurts." It's like trying to grease a misaligned gear.

Architectural Refactoring (Architectural Fix): Separate the Sync State (synchronous state like input text) and Async State (asynchronous state like search results). Instead of forcing the system to know what the User is typing every millisecond, let the Search Component manage its own "noise." Only when the "waters are calm" should the data be pushed into the System Flow.

4. Technical Debt and Self-Reflection

Placing debounce in the wrong place is a silent technical debt. It makes the system look fine at a small scale but will shatter as the number of components increases. Then, you'll have to use dozens of useMemo and useCallback just to stay afloat.

Sometimes I wonder: "Am I making the system more complex than necessary?". Why should an input box affect such a large system? Perhaps using Global State (like Redux or Context) for "temporary" data like input text was the architectural mistake from the start.

Lesson Learned

Performance issues rarely lie within a single function or algorithm. They lie in the coordination between components. Don't just look for ways to make a component run faster. Look for ways for it to bother other components as little as possible.

Good architecture is one where data only flows to where it's needed, at the exact moment it's needed.


Notes on the necessary silence in the state flow.

Previous: Blog #15: The 10,000-Row Lesson – When Table Virtualization saved my careerAll posts in this seriesNext: Blog #17: Images vs JS – The battle for priority in the Critical Path