skip to content

What does { passive: true } promise the browser when you register a DOM listener with addEventListener, and what happens if that listener calls preventDefault()?

level: seniorimportance: should knowfreq 48%

answer

  1. a promise not to cancel
  2. the compositor stops waiting for you
  3. preventDefault becomes a no-op
  4. document-level touch and wheel already default
  5. opt out with passive: false

basics

~20 s

It promises the listener will never cancel the event's default action, so the browser can start scrolling immediately instead of waiting to see what the listener does. preventDefault() inside a passive listener is a no-op; Chrome logs a console warning when you try.

solid answer

~50 s

For scroll-driving inputs such as `touchstart`, `touchmove` and `wheel`, the browser cannot begin scrolling until it knows whether a listener will cancel the gesture — so with a normal listener it must run your JavaScript on the main thread first, and any delay there is felt as scroll latency. `{ passive: true }` is a promise that the listener will not call `preventDefault()`, which lets the compositor scroll in parallel with the handler. If the listener calls `preventDefault()` anyway, the call is ignored; Chrome reports "Unable to preventDefault inside passive event listener invocation". Because so much code got this wrong, current browsers make those event types passive **by default** when the listener is added to `window`, `document`, `documentElement` or `body`, so code that genuinely needs to cancel a gesture there must opt out with `{ passive: false }`. Passive is a registration option only — it plays no part in matching a `removeEventListener` call.

code

javascript · 9 lines
javascript
// Observer: never cancels, so let the browser scroll in parallel
window.addEventListener('wheel', () => {
  document.documentElement.dataset.scrolling = 'true';
}, { passive: true });

// Controller: must cancel, so opt out of the document-level default
document.addEventListener('touchmove', (event) => {
  if (event.cancelable) event.preventDefault();
}, { passive: false });

go deeper

for a junior

Know that passive: true tells the browser the listener will not call preventDefault, that the browser then ignores preventDefault if you call it anyway, and that it exists to keep scrolling smooth.

for a middle

Explain the mechanism: without the promise the browser must run the listener before it knows whether the scroll is cancelled, so the compositor waits; with it, scrolling proceeds in parallel.

for a senior

Show the production judgment — classify each listener as observer or controller, opt out with passive: false only where a gesture is genuinely cancelled, and recognise that passive does not excuse a slow handler.

for a principal

Own the tradeoff at scale: third-party scripts attaching document-level handlers are why the default changed, so decide what your app permits, and prefer declarative controls such as touch-action over cancelling every move event.

## Why the browser waits for your listener Modern browsers scroll on a compositor thread that runs independently of the main thread, which is why a page can usually keep scrolling smoothly while JavaScript is busy. The exception is when the page might *cancel* the scroll. `touchstart`, `touchmove` and `wheel` all have a default action — panning the page — and a listener is allowed to call `event.preventDefault()` to suppress it, which is how custom gestures, drag surfaces, carousels and pull-to-refresh implementations work. The browser cannot know in advance whether your listener will do that. So when a cancellable scroll-driving event has a listener attached, it must dispatch the event to the main thread and wait for the handler to return before it can decide to scroll. If the main thread is busy, or the handler itself is slow, the scroll does not start until it finishes. Users perceive this as the page "sticking" to the finger and then catching up. ## What passive promises ```js el.addEventListener('touchmove', onTouchMove, { passive: true }); ``` `passive: true` declares up front: this listener will not cancel the default action. That single bit of information is enough for the browser to stop waiting — it can begin scrolling on the compositor at once and run your listener in parallel. The listener still receives the full event object and can read coordinates, update state, or measure; it simply cannot veto the gesture. The guarantee is enforced, not merely documented. Inside a passive listener the browser sets an internal flag, and `event.preventDefault()` becomes a no-op. Chrome additionally logs a console warning — "Unable to preventDefault inside passive event listener invocation" — which is often the first hint developers get that a listener they wrote is passive without their knowledge. ## The document-level default So much scroll jank came from analytics snippets and libraries attaching untuned `touchstart`/`touchmove` handlers to the document that current browsers changed the default. When the event type is `touchstart`, `touchmove`, `wheel` or `mousewheel` **and** the listener is added to `window`, `document`, `documentElement` or `body`, the listener is passive unless you explicitly say otherwise. That has a real consequence: this no longer prevents page scroll in a current browser: ```js document.addEventListener('touchmove', (e) => e.preventDefault()); // ignored ``` You have to opt out deliberately: ```js document.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false }); ``` Note the default applies to those *document-level* targets only. A listener added directly to an inner element is not passive by default; there, if you want the performance benefit you must ask for it. ## Deciding which listeners are passive The rule of thumb: a listener that only *observes* the gesture should be passive, and a listener that *controls* the gesture must not be. Sticky headers, scroll-progress indicators, parallax readouts and telemetry are observers — mark them passive. A canvas that implements its own pan-and-zoom, a drag surface that must not scroll the page underneath, and a custom pull-to-refresh are controllers — they need `{ passive: false }`, and the cost is that the browser waits for them, so their handlers have to be genuinely fast. A useful third option exists in CSS: when the goal is simply "do not scroll here", `touch-action: none` on the element tells the browser before any event is dispatched, which is both cheaper and more reliable than cancelling `touchmove` from JavaScript. Reaching for `preventDefault()` on every move event is the older, heavier approach. ## Gotchas worth carrying into an interview - **Passive does not make a slow handler harmless.** It unblocks scrolling, but the callback still runs on the main thread and still contributes to long tasks. A passive `touchmove` handler that reads layout every frame will still make the page feel bad. - **Passive is not part of removal matching.** `removeEventListener` compares type, callback and capture only, so you never pass `passive` when detaching. - **Check before you cancel.** `event.cancelable` is the honest test for whether a default action can still be suppressed; browsers dispatch some in-progress scroll events as non-cancellable regardless of your options. - **Marking every listener passive is not a free optimisation.** It matters for the scroll-driving event types; on a `click` listener it changes nothing about scheduling and only silently breaks any `preventDefault()` you later add. - **Feature detection has a history.** Because the third argument used to be a boolean, older code detected options-object support by passing an object with a `passive` getter and observing whether the browser read it. In any current browser that check is dead code. The short version an interviewer wants: passive removes the browser's need to ask your JavaScript for permission before scrolling, the browser enforces the promise by ignoring `preventDefault()`, and document-level touch and wheel listeners already have it on by default in current browsers.

  • Which listeners are passive by default in current browsers, and why was that default introduced?
    touchstart, touchmove, wheel and mousewheel listeners added to window, document, documentElement or body. Too much third-party script attached untuned handlers at that level, and each one forced the browser to wait on the main thread before scrolling. Making the document-level case passive by default recovered scroll responsiveness without every site changing its code.
  • You need a custom gesture on document that genuinely must stop the page scrolling. What do you write?
    Register with `{ passive: false }` so preventDefault() is honoured, and keep the handler fast — the browser now waits for it before scrolling. Where the intent is simply "no scrolling in this region", prefer the CSS touch-action property, which tells the browser before dispatch instead of cancelling every move event.
  • Does marking a listener passive make a slow handler stop causing jank?
    No. It only removes the scroll-blocking dependency: the compositor can scroll while your handler runs. The callback is still main-thread work and still contributes to long tasks, so a passive handler that reads layout on every move will still make interactions feel slow.
  • Do you need to pass passive to removeEventListener to detach a passive listener?
    No. Removal compares only the event type, the callback reference and the capture flag; passive, once and signal are ignored during matching. Pass the same function and the same capture value and the passive listener comes off.

saying these in an interview costs you the question

  • Thinks passive makes the handler run off the main thread
  • Believes preventDefault still works inside a passive listener
  • Says passive: true is a free win on every listener
  • Unaware document-level touch and wheel default to passive
  • Confuses passive with once or with capture

context