Debouncing Window Resize and Scroll with Lodash
Use Lodash _.debounce to run expensive resize and scroll handlers only after the user stops interacting.
27 May 2026, 10:29 UTC

The problem: resize and scroll fire constantly
Attaching a handler directly to window.resize or window.scroll means the browser can invoke it dozens of times per second. If that handler reads layout properties, recalculates positions, or triggers a network request, the page can jank or overload the server. You need the work to run only after the user stops moving, not on every intermediate pixel.
What _.debounce does
_.debounce from Lodash 4.x wraps a function so it waits for a quiet period before running. Each time the wrapped function is called, it resets an internal timer. The underlying function executes only after wait milliseconds pass with no new calls. Think of it as a pause-and-reset switch: rapid calls collapse into a single execution at the end of the pause.
Two terms matter here:
- Leading edge: the moment the debounced function is first called.
- Trailing edge: the moment after the wait period ends and no new call has arrived.
Leading, trailing, and maxWait
By default, _.debounce fires on the trailing edge only. The first call starts the timer but does not run the function. If you need immediate feedback on the first interaction, set leading: true. If you need an upper bound on how long the work can be delayed, set maxWait; the function is guaranteed to run at least once every maxWait milliseconds even if events keep arriving.
Missing the final call is a common surprise. With leading: true, trailing: false, the last burst of events may never produce a final execution. With leading: false, trailing: true, the user waits for the full wait period after they stop before anything happens.
Worked example: debounced resize handler
Run this in your frontend source or paste it into the browser DevTools console after loading Lodash. No special permissions are required.
import debounce from 'lodash/debounce';
function recalculateLayout() {
const width = window.innerWidth;
console.log('Recalculating layout at width:', width);
// Expensive DOM reads or writes go here.
}
// Wait 200 ms after the last resize event before running.
// leading: false means no immediate call on the first event.
// trailing: true means one final call after the user stops.
const debouncedResize = debounce(recalculateLayout, 200, {
leading: false,
trailing: true,
});
window.addEventListener('resize', debouncedResize);
Expected check: open the DevTools console, resize the window, and watch the log. You should see far fewer messages than resize events, and the last message should appear roughly 200 ms after you stop dragging the window edge.
Risk: because this example uses trailing-only execution, the user perceives a 200 ms delay before the layout updates. If your UI needs an immediate response, add leading: true, but then decide whether you also want trailing: true so the final state is still applied.
To stop listening later, keep the wrapper reference and cancel any pending invocation:
debouncedResize.cancel();
window.removeEventListener('resize', debouncedResize);
The this-binding gotcha
Debounced functions lose their original this context. If recalculateLayout were a method on a class or object, passing it directly to _.debounce can break internal references. Use an arrow function wrapper or bind the method explicitly:
const debouncedResize = debounce(
() => this.recalculateLayout(),
200,
{ leading: false, trailing: true }
);
Importing without bloating the bundle
Importing lodash/debounce pulls in only that function and its small dependencies, which helps tree-shaking in modern bundlers. If you use ESM, lodash-es is another option. Verify with your bundle analyzer that the debounce module appears as a separate, small chunk rather than the full Lodash library.
Check that it is working
- Load the page with the debounced handler attached.
- Open DevTools and monitor console output or add a counter inside the wrapped function.
- Trigger the event rapidly, then stop.
- Confirm the underlying function runs once after the pause, not once per event.
- If you set
maxWait, confirm the function still runs within that upper bound even during continuous events.
Limitations
_.debouncedelays work; it does not reduce the number of events the browser delivers.- It is not a throttle. Throttling guarantees a maximum rate; debouncing guarantees a quiet period.
- A long
waitcan make the UI feel sluggish. Start with 100–250 ms for resize and scroll, then measure. - The wrapped function exposes
cancelandflush, but you must keep a reference to the wrapper to call them.
Use debounce when the user’s final state matters more than every intermediate step. If you need consistent progress updates while the user drags, throttle is usually the better fit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.