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 #16: Misplaced Debounce – When a localized solution becomes a system burden
50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

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.

TP
Truong PhamSoftware Engineer
PublishedApril 22, 2024

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.

Series • Part 16 of 50

50 FRONTEND LESSONS – HARD-EARNED EXPERIENCES

NextBlog #17: Images vs JS – The battle for priority in the Critical Path
Blog #15: The 10,000-Row Lesson – When Table Virtualization saved my career
11Blog #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 see14Blog #14: The Domino Effect – When one small change crashes the whole Render system15Blog #15: The 10,000-Row Lesson – When Table Virtualization saved my career16Blog #16: Misplaced Debounce – When a localized solution becomes a system burdenReading17Blog #17: Images vs JS – The battle for priority in the Critical Path18Blog #18: Laggy Animations – When the touch is no longer smooth19Blog #19: Chrome Performance Tab – Looking through a microscope at the system's pulse20Blog #20: The Mystery of LCP – Why do 'fast' websites still get rated low?21Blog #21: The Global State Nightmare – The decision to 'topple' the Redux monument
TP

Written by Truong Pham

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

Read more articles