How to Improve Scroll Performance with Passive Event Listeners

How do passive event listeners improve scroll performance? Passive event listeners improve scroll performance by promising the browser that your handler won’t call preventDefault(), so the browser can scroll immediately instead of waiting for your code to run.

Tarun Sharma
Tarun Sharma Founder, Chetaru
|
Updated Jun 23, 2026
|
7 min read
Share

Need More Growth & Leads?

We are ready to work with your business and generate some real results…

Let's Talk

How do passive event listeners improve scroll performance?

Passive event listeners improve scroll performance by promising the browser that your handler won’t call preventDefault(), so the browser can scroll immediately instead of waiting for your code to run. That wait is the whole problem: to hit a smooth 60 frames per second, the browser has roughly 16 milliseconds to produce each frame (web.dev), and a blocking scroll or touch listener can eat straight into that budget. Marking the listener { passive: true } removes the wait.

Key Takeaways

  • A passive listener tells the browser your handler will never call preventDefault(), so scrolling never has to wait for your JavaScript (MDN).
  • Since Chrome 56 (2017), touchstart and touchmove listeners on the document, window, and body are passive by default (Chrome for Developers).
  • The browser aims for one frame every ~16ms to keep scrolling at 60fps; a blocking listener can miss that window (web.dev).
  • Lighthouse has a dedicated audit, “Does not use passive listeners to improve scrolling performance,” that flags the listeners worth fixing (Chrome Lighthouse).

Scroll responsiveness is part of how Google measures real-user experience through Interaction to Next Paint, where a good score is 200 milliseconds or less at the 75th percentile (web.dev). This guide explains what passive listeners are, why blocking ones cause jank, exactly how to add them, and which events to apply them to. It pairs well with our wider guide to improving Core Web Vitals.

What are passive event listeners?

A passive event listener is one you register with the { passive: true } option, which signals to the browser that the listener will never call preventDefault() (MDN). The option was added to addEventListener through the DOM specification and shipped in Chrome 51 in 2016, and it’s now supported in every current browser (MDN browser compatibility).

The default behaviour is the opposite. When you attach an ordinary scroll, touch, or wheel listener, the browser has no way of knowing in advance whether your code will cancel the default action, so it has to play it safe. Passive listeners remove that uncertainty by stating your intent up front.

Why do regular scroll and touch listeners slow scrolling down?

A non-passive listener slows scrolling because the browser must run your JavaScript and confirm you didn’t call preventDefault() before it’s allowed to scroll the page (Chrome for Developers). preventDefault() cancels an event’s default action, and for a scroll or touch event that default action is the scroll itself, so the browser can’t assume anything until your handler finishes.

That creates a chain of delay. The user moves their finger or wheel, the browser hands the event to your listener, your code runs, and only then, once the browser sees no cancellation, does the page move. If your handler does meaningful work, the scroll stutters. The effect is worst on touch devices, which is exactly why mobile scrolling is where passive listeners pay off most.

How do you add a passive event listener?

You add a passive listener by passing an options object as the third argument to addEventListener, with passive set to true (MDN):

window.addEventListener('scroll', function () {
  // your scroll handling code
}, { passive: true });

The same pattern works for touch and wheel events, which are the other common culprits behind janky scrolling:

document.addEventListener('touchmove', onTouchMove, { passive: true });
document.addEventListener('wheel', onWheel, { passive: true });

One gotcha is worth knowing before you flip every listener to passive. If you mark a listener passive and then call preventDefault() inside it, the browser ignores the call and logs a console warning rather than honouring it (MDN). So a listener that genuinely needs to cancel scrolling, a custom swipe-to-dismiss gesture, for example, must stay non-passive. Passive is a promise, and the browser holds you to it.

Which events should be passive, and which shouldn’t?

Most scroll-adjacent events should be passive, because the vast majority of handlers only read the scroll position or trigger an effect rather than cancelling the scroll (Chrome for Developers). The exceptions are the handful of listeners that deliberately block the default action. The table below covers the common cases.

Event Make it passive? Why
scroll Yes Scroll handlers can’t cancel scrolling anyway, so there’s no reason to block
touchstart / touchmove Usually Passive by default at document level since Chrome 56; keep passive unless you handle gestures
wheel / mousewheel Usually Passive unless you implement custom zoom or scroll hijacking
Custom swipe or pull-to-refresh No These call preventDefault() to control the gesture, so they must stay non-passive

If you’re unsure, default to passive and test the interaction. A listener that breaks when made passive will tell you immediately, because the gesture it controls will stop working and the console will warn you.

What does Chrome do automatically?

Chrome already makes some of these listeners passive for you. Since Chrome 56, released in early 2017, touchstart and touchmove listeners added to the document, window, or body are treated as passive by default (Chrome for Developers). Firefox and Safari adopted the same document-level default, so this behaviour is now consistent across browsers (MDN).

The important limit is scope. This automatic default only applies to those top-level targets. A touchmove listener attached to a specific <div> or any other element is still non-passive unless you say otherwise, so element-level listeners are where you still need to add { passive: true } by hand. That distinction catches a lot of developers who assume the whole page is covered.

How do you find non-passive listeners on your site?

Run Lighthouse, which includes an audit named “Does not use passive listeners to improve scrolling performance” that lists the listeners worth converting (Chrome Lighthouse). Lighthouse is built into Chrome DevTools, so you can open the Lighthouse panel, run a report, and read the flagged listeners straight from the best-practices section.

For a closer look, the DevTools Performance panel records what happens during a scroll and shows long-running event handlers on the main thread, which is where you’ll see a listener blocking a frame. Between the Lighthouse audit and a recorded scroll trace, you can pinpoint exactly which handlers are costing you smoothness before you change a line of code.

What else improves scroll performance?

Passive listeners are one lever among several, and the biggest scroll wins usually come from reducing main-thread work overall (web.dev). Once your listeners are passive, these techniques compound the benefit:

  • Load JavaScript with async or defer so scripts don’t block rendering while the page is still drawing. Our guide to reducing unused JavaScript goes deeper on trimming what loads.
  • Optimise images and media. Compress files, serve modern formats like WebP, and add loading="lazy" so off-screen images don’t load until they’re needed.
  • Trim and minify CSS and JavaScript. Smaller files parse faster and leave more of the frame budget for scrolling. See how to identify and reduce unused CSS.
  • Keep work out of scroll handlers. Avoid layout reads and heavy calculations inside a scroll callback; throttle the work or move it behind requestAnimationFrame.
  • Remove listeners you no longer need with removeEventListener, so old handlers don’t keep firing.

If a poor Lighthouse score is what brought you here, our walkthrough on improving your Lighthouse performance score ties these fixes together.

Frequently asked questions

Yes. The passive option is supported in all current browsers, including Chrome, Firefox, Safari, and Edge, and has been since around 2016 (MDN). Older browsers that don’t recognise the options object simply ignore it and treat the listener as non-passive, so adding { passive: true } is safe to ship without a fallback.

Final thoughts

Passive event listeners are one of the cheapest performance wins available: a single option that stops scroll and touch handlers from blocking the browser. Add { passive: true } to listeners that don’t call preventDefault(), leave the ones that do alone, and let Chrome’s document-level default cover the rest. Run a Lighthouse report to confirm the change landed, then verify the improvement in real-user field data over the following weeks. If scrolling still feels heavy, the next place to look is the main thread itself, where reducing JavaScript and image work tends to deliver the larger gains.