All topics
Performanceintermediate

Passive Event Listeners and Scroll Performance

The passive option for addEventListener that tells the browser a handler won't call preventDefault, letting scrolling proceed immediately without waiting.

The passive option for addEventListener is a small but meaningfully impactful performance optimization specifically for scroll-related events (touchstart, touchmove, and wheel in particular), addressing a subtle problem: by default, the browser can't know in advance whether an event handler is going to call preventDefault() to cancel the event's default action (like scrolling), so it has to wait for the handler to finish running before it can safely proceed with that default action — even if the handler never actually calls preventDefault() at all.

A passive listener is like telling a security guard in advance 'I'm just here to observe, I promise not to stop anyone' — the guard (browser) can let foot traffic (scrolling) continue flowing immediately without pausing to watch what you do, instead of the default assumption that any observer might suddenly step in front of someone and needs to be watched closely before traffic can proceed.

Key Concepts

1
For scroll-triggering events specifically, this default wait-and-see behavior is a real performance problem: if a touchmove or wheel listener is attached anywhere in the scroll path, the browser must synchronously run that listener to completion before it can start scrolling, even though the overwhelming majority of such listeners never actually call preventDefault() — they're just passively observing the scroll (for analytics, or triggering some unrelated side effect) without any intention of blocking it. This forced wait can introduce a visible, janky delay to what should be an instant, fluid scrolling response.
touchmovewheelpreventDefault()
2
Passing { passive: true } as part of the options object (element.addEventListener('touchmove', handler, { passive: true })) is a promise to the browser that the handler will *never* call preventDefault(), which frees the browser to begin the default scrolling behavior immediately, in parallel with running the handler, rather than waiting for it to finish first — since the browser now knows in advance there's no chance the handler will try to cancel that default behavior. If a handler marked passive: true does call preventDefault() anyway, the call is simply ignored (in most browsers, with a console warning), rather than throwing an error, since honoring it at that point would violate the exact guarantee the option promised.
{ passive: true }element.addEventListener('touchmove', handler, { passive: true })preventDefault()passive: true
3
Modern browsers have actually made touchstart/touchmove listeners passive *by default* specifically to improve scroll performance out of the box without requiring every website to opt in explicitly, though wheel events and listeners attached in other contexts may still require the explicit option, and understanding why this default exists — and being able to explicitly opt in for other event types or contexts — remains a genuinely practical, real performance detail worth knowing.
touchstarttouchmovewheel