Debounce, throttle, and yield in event handlers
Use debouncing, throttling, passive event listeners, and requestAnimationFrame to reduce event handler execution time and improve Interaction to Next Paint (INP).
Event handlers execute synchronously on the main thread. Expensive handlers block the browser from responding to user input, directly harming INP. Common problems include running heavy computations on every scroll or resize event, performing synchronous DOM measurements in handlers, executing multiple layout-triggering operations, and running expensive operations on every keystroke.
INP measures the time from user interaction to visual update. Event handler execution is part of the Processing Time phase, one of three phases that contribute to INP (Input Delay, Processing Time, and Presentation Delay). Optimizing handlers reduces Processing Time, which directly improves INP scores.
Anchor to DebounceDebounce
Debouncing delays execution until the user stops the action. Use it for search inputs, resize handlers, and form validation.
Anchor to ThrottleThrottle
Throttling limits execution frequency. Use it for scroll handlers, mouse move tracking, and resize events.
Anchor to Use passive event listenersUse passive event listeners
Passive listeners tell the browser that the handler won't call preventDefault(), allowing the browser to optimize scrolling and touch handling.
Anchor to Use ,[object Object], for visual updatesUse requestAnimationFrame for visual updates
requestAnimationFrame for visual updatesUse requestAnimationFrame to batch DOM updates and sync with browser paint cycles.
Anchor to Break up long tasksBreak up long tasks
Split long-running handlers into chunks using setTimeout or scheduler.yield() when available.
Anchor to Yield to the main thread for non-critical updatesYield to the main thread for non-critical updates
Defer non-critical updates, such as URL changes and analytics tracking, to avoid blocking the interaction response.
URL updates can trigger tracking pixels synchronously, delaying interactions:
When to use:
- URL updates, such as
history.pushStateorhistory.replaceState. - Analytics tracking calls.
- Non-visual state updates.
- Any update that triggers third-party scripts.
Impact: Prevents Facebook, GTM, and other tracking pixels from blocking interactions.
Anchor to Batch DOM reads and writesBatch DOM reads and writes
Group all DOM reads together, then all DOM writes, to avoid layout thrashing from forced synchronous layouts.
Performance profiling of slideshow and carousel components has shown that alternating DOM reads and writes causes expensive layout thrashing. Batching all reads before writes within requestAnimationFrame can improve page load times by 300 ms or more, especially on pages with multiple slideshows.
Impact: Measured improvements of 300 ms or more on complex pages with multiple interactive components.
Common layout-triggering properties (reads):
offsetWidth,offsetHeight,offsetTop, andoffsetLeft.clientWidthandclientHeight.scrollWidth,scrollHeight,scrollTop, andscrollLeft.getBoundingClientRect().getComputedStyle().
Avoid alternating reads and writes. Batch all reads, then all writes.
Anchor to Avoid hover styles on touch devicesAvoid hover styles on touch devices
Use @media (hover: hover) to prevent hover styles from triggering on mobile taps.
Performance profiling has shown that hover styles trigger expensive style recalculations on mobile devices when users tap elements. Mobile browsers apply :hover pseudo-class styles during tap interactions, causing unnecessary opacity transitions and style recalculations that delay the interaction response.
Why this matters:
The hover media query detects whether the device has hover capability (mouse or trackpad) or is touch-only. By wrapping hover styles in this media query, you prevent expensive animations from triggering on mobile taps.
Impact: Eliminates expensive opacity and transform animations from mobile tap interactions. Most noticeable on header and navigation elements.
Anchor to Offload to Web WorkersOffload to Web Workers
Move computationally expensive work off the main thread entirely.
Anchor to ExamplesExamples
Anchor to Real-world: product filterReal-world: product filter
Anchor to Real-world: infinite scrollReal-world: infinite scroll
Anchor to Real-world: parallax effectReal-world: parallax effect
Anchor to TestingTesting
- Chrome DevTools Performance panel: Record interactions and identify long tasks in event handlers.
- INP debugger: Use the
web-vitalslibrary to log INP issues in development. - Field data: Monitor INP in Shopify's Web Performance dashboards or CrUX data after optimization.
Anchor to ReferencesReferences
- Optimize long tasks
- Debouncing and throttling explained
- Using passive event listeners
- requestAnimationFrame
- Scheduler.yield() proposal
- Using Web Workers
- hover media query
- Understanding INP
- Audit and remove third-party scripts
- Defer non-critical scripts
- Optimize view transitions for better interactions