cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

How input reaches your event handlers

Learn how browser input reaches event handlers, why passive listeners protect scrolling, and how hit testing and pointer samples work.

By the end, you can
  • 01
    Explain scroll routingDescribe when the compositor can start a scroll and when it must wait for the main thread.
  • 02
    Choose listener optionsUse passive listeners honestly and check whether an event is cancelable before relying on preventDefault.
  • 03
    Handle precise inputFind targets with hit testing and feature-detect coalesced and predicted pointer samples.

One input, several decisions

When a finger moves, a wheel turns, or a mouse clicks, the browser first receives coordinates and an event type. It must decide whether it can update the screen quickly, whether JavaScript must get a chance to cancel something, and which page element should receive the event.

Definition

The input pipeline is the browser work that routes physical input to a page, finds a target, runs listeners when needed, and performs default actions such as scrolling.

This lesson builds on the rendering pipeline. Rendering prepared layers lets a compositor assemble frames quickly. Input asks whether that same compositor can keep moving a scroll, or whether it must wait for the renderer main thread.

The words in this lesson describe a browser design, not an extra JavaScript event loop. Your handler still runs on the main thread. The useful question is whether the browser must wait for that handler before starting a default action.

Input handled on the compositor thread

The compositor thread combines already-painted layers into frames. For a scroll it can often move those layers without asking the main thread to run JavaScript or recalculate layout. That separation is one reason a page can keep scrolling while other work is busy.

Observe a wheel eventPop out in the code editor (opens in a new tab)JavaScript
window.addEventListener("wheel", () => console.log("wheel seen"), {  passive: true,});console.log("listener added");

Line 1 registers a listener on window. Line 2 marks it passive, which is a promise that it will not cancel the wheel event. Line 4 runs immediately and prints listener added. Later wheel input prints wheel seen in the console.

The listener is still useful for observation, analytics, or updating a small visual indicator. Passive does not mean skipped. It means the browser may start the default scroll without waiting to learn whether this particular listener called preventDefault().

Real-life analogyA security guard at a building gate

A security guard can wave cars through a gate at once. At a gate where the office said “call me before letting anyone in”, every car waits while the office answers. A passive listener is telling the guard there is no need to call.

In real life: Guard waves cars through
In JavaScript: Compositor starts a scroll
In real life: Office may stop a car
In JavaScript: Main thread may cancel a default action
In real life: Guard needs no call
In JavaScript: A passive listener cannot cancel
In real life: Cars wait for the office
In JavaScript: A non-fast region waits for JavaScript

Where the analogy stops: Browsers use detailed event and rendering rules; the guard is only a model for the wait-or-proceed decision.

Non-fast scrollable regions

A non-fast scrollable region is an area where a potentially canceling input listener means the compositor may need main-thread confirmation before it starts scrolling. Chrome records this information while it builds and composites the page.

Mark a tiny set of scroll regionsJavaScript
const listeners = [  { target: "cart", type: "wheel", passive: false },  { target: "menu", type: "touchmove", passive: true },  { target: "map", type: "pointermove", passive: false },]; const regions = markScrollableRegions(listeners);console.log(regions.map((region) => region.region).join(","));

Line 2 gives cart a non-passive wheel listener, so the model marks it non-fast. Line 3 gives menu a passive touch listener, so it is fast. Line 4 is a pointer listener: it can run JavaScript, but it is not one of this small model's scroll-blocking listener types.

The result prints non-fast,fast,fast. This is a teaching model, not Chrome's region map. It makes one useful rule visible: non-passive wheel, touchstart, and touchmove listeners are the usual listeners that can require a scroll-start decision.

cartnon-fast

wheel may cancel

menufast

touchmove cannot hold up scroll in this model

mapfast

pointermove cannot hold up scroll in this model

A wheel, touch, or pointer listener that could call preventDefault() changes the decision. The browser cannot know your function will be quick, and it cannot know it will decide not to cancel until it runs. A busy main thread therefore becomes visible as scroll-start delay.

Step through one non-passive wheel event
Step 0 of 5Ready
Your turn: follow the blue line

Replay one wheel event through a small, deterministic scroll-start model.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const mainThreadBusyMs = 40; function startScroll(passive, mainThreadBusyMs) {  return passive ? 0 : mainThreadBusyMs;} console.log(startScroll(passive, mainThreadBusyMs));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay uses a tiny deterministic model. It does not measure Chrome, but it makes the dependency plain: a non-passive listener has a wait equal to the modeled busy time; a passive listener has a zero scroll-start wait. Real browsers add more detail, including event type and CSS.

Passive listeners and cancelable events

A cancelable event has a default action that a listener may be able to stop. Read event.cancelable before writing cancellation logic. Calling preventDefault() on a non-cancelable event does not change its default action.

Try to cancel inside a passive listenerPop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget(); target.addEventListener("checkout", (event) => {  console.log("cancelable", event.cancelable);  event.preventDefault();  console.log("defaultPrevented", event.defaultPrevented);}, { passive: true }); target.dispatchEvent(new Event("checkout", { cancelable: true }));

Line 3 adds a passive listener. Line 4 first prints cancelable true. Line 5 asks to prevent the default action, but that request is ignored because the listener promised not to cancel. Line 6 therefore prints defaultPrevented false in browsers.

Node's EventTarget does not model passive cancellation the way browser DOM does, so the lesson test proves this snippet in Chrome. Do not use a passive listener when cancellation is genuinely part of the feature. Prefer CSS such as touch-action when it describes the gesture policy more directly.

Make the listener promise explicitJavaScript
// State the option instead of depending on a browser default.document.addEventListener("wheel", recordScroll, { passive: true });document.addEventListener("touchstart", startPan, { passive: false }); console.log("wheel observes; touchstart may cancel");

Line 2 explicitly says the wheel listener only observes. Line 3 explicitly says this touch listener may cancel because the feature needs that choice. Line 5 prints wheel observes; touchstart may cancel. Explicit options are clearer than relying on a target-specific default.

MDN documents that modern browsers other than Safari commonly default document-level wheel, touchstart, and touchmove listeners to passive. That is a compatibility behavior, not a reason to omit the option: use passive: false when cancellation is intentional and test the gesture on the browsers you support.

Hit testing finds the target

Hit testing asks which painted, eligible element is underneath a viewport coordinate. When an input must reach JavaScript, the main thread uses rendering information to find the event target before event dispatch follows capture, target, and bubble rules.

Find an element under a pointPop out in the code editor (opens in a new tab)JavaScript
const back = document.createElement("button");back.textContent = "Pay";back.style.cssText = "position:fixed;left:20px;top:20px;width:120px;height:60px"; const cover = document.createElement("div");cover.style.cssText = "position:fixed;left:20px;top:20px;width:120px;height:60px;pointer-events:none"; document.body.append(back, cover);console.log(document.elementFromPoint(40, 40)?.textContent);

Lines 1 through 4 build a button. Lines 5 through 7 build a cover directly over it, but the cover has pointer-events: none. Line 9 asks for the element at (40, 40) and prints Pay, because the cover is invisible to hit testing.

This is useful for decorative overlays, labels, and effects that should not block a real control beneath them. It does not remove the cover from the DOM or stop events that target another element from bubbling through ancestors in their normal way.

A component can also place its real control inside a shadow tree. Hit testing may find that inner control, while a listener outside the shadow tree can see event.target retargeted to the component host. That boundary keeps component internals private; use the event's composed path only when your component contract needs that extra detail.

Coalesced and predicted pointer events

Touch and pointer hardware can report movement more often than the display draws frames. Delivering every move to JavaScript would create too much hit testing and handler work. Browsers can merge several moves into one delivered pointermove near a frame.

Feature-detect extra pointer samplesPop out in the code editor (opens in a new tab)JavaScript
window.addEventListener("pointermove", (event) => {  const points = event.getCoalescedEvents?.() ?? [event];  const guesses = event.getPredictedEvents?.() ?? [];  console.log("points", points.length);  console.log("guesses", guesses.length);});

Line 2 reads historical points with optional chaining, falling back to the delivered event. Line 3 asks for estimated future points only when that method exists. Lines 4 and 5 print the counts. The fallback makes the code safe in browsers that do not support one or both APIs.

getCoalescedEvents() returns earlier real points merged into this delivery and is valuable for a drawing path. Predicted events are estimates of future positions based on recent velocity and direction. They can reduce perceived drawing latency, but later real input can disagree.

Model several raw moves delivered togetherJavaScript
const rawMoves = [  { x: 10, y: 10, time: 0 },  { x: 14, y: 10, time: 4 },  { x: 18, y: 10, time: 8 },  { x: 22, y: 10, time: 12 },]; const deliveries = coalesceRawMoves(rawMoves, 3);console.log(deliveries.length);console.log(deliveries[0].coalesced.length);console.log(deliveries[0].delivered.x);

Lines 2 through 5 describe four raw moves. Line 7 groups them in threes. Line 8 prints 2 because two JavaScript deliveries are enough for four raw moves. Line 9 prints 3, and line 10 prints 18, the last point in the first delivered group.

In browser code, the delivered event is the final point and getCoalescedEvents() can provide the earlier positions. Draw every returned real point in order. Keep a predicted point separate, because the next actual delivery may require you to correct that temporary stroke.

Step through pointer samples
Step 0 of 4Ready
Your turn: follow the blue line

Replay a small sample-count model. Browsers choose when and how many points to coalesce or predict.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function samplesFor(event) {  return {    points: event.coalesced,    guesses: event.predicted,  };} console.log(samplesFor(event));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Support is not uniform: MDN marks coalesced events as limited availability, and predicted events as newly available baseline support. Feature detection is therefore more honest than assuming every pointer event has these methods or returns the same number of samples.

Try the scroll-start model

Use this playground to change one decision at a time. Passive means the compositor may start scrolling right away in this model. “May cancel” means it waits for the stated main-thread busy time, because JavaScript could call preventDefault().

Playground: will this wheel input wait?
Scroll-start modelPop out in the code editor (opens in a new tab)JavaScript
const passive = true;const mainThreadBusyMs = 40; function startScroll(passive, mainThreadBusyMs) {  return passive ? 0 : mainThreadBusyMs;} console.log(startScroll(passive, mainThreadBusyMs));
Modeled result
routefast region | compositor | wait 0ms | immediately

Change only the listener promise or busy time, then compare when scrolling may start.

Try it yourself

The compositor can begin default scrolling immediately. The busy time still affects other work, but this listener does not hold up scroll start.

A deterministic teaching model. Real browsers also consider event type, target, CSS such as touch-action, and browser-specific implementation details.

This is a teaching model rather than a profiler. It deliberately has only two inputs so the causal rule remains visible. On a real page, inspect the event type, listener options, target, CSS gesture policy, and a performance trace before deciding what caused jank.

Practical listener choices

Mark a listener passive when it only observes an event. A wheel analytics callback, a visual readout, or a listener that schedules ordinary work is often a good candidate. Write the option explicitly so a future reader does not depend on browser-specific default-passive behavior.

Keep a non-passive listener only when cancellation is truly required. For touch gestures, first ask whether touch-action can describe the browser gesture policy. CSS lets the browser know earlier than a JavaScript handler that it may need to stop or allow a pan.

Fix a janky scroll handler

Start with the smallest useful change. A cart preview that only observes wheel movement should not save data inside every wheel callback, and it should not leave the browser guessing whether the callback might cancel scrolling.

Before: a wheel listener that may hold up scrollingJavaScript
window.addEventListener("wheel", (event) => {  updateCartPreview(event.deltaY);  saveCart();  console.log("preview updated");});

Line 1 gives no passive promise. Line 2 updates a preview, line 3 starts a save, and line 4 prints preview updated. Saving work on every wheel delivery can make the main thread busy even when the handler never calls preventDefault().

After: observe passively and schedule visual workJavaScript
window.addEventListener("wheel", (event) => {  requestAnimationFrame(() => updateCartPreview(event.deltaY));  console.log("preview scheduled");}, { passive: true });

Line 2 batches the preview update for a rendering frame. Line 3 prints preview scheduled, and line 4 promises this listener will not cancel. Debounce or save after the gesture separately; passive avoids scroll-start waiting, but it does not make expensive work free.

Four input concepts that look similar
ConceptMeaningWhat it changes
Passive listenerPromises it will not cancel the default action.The browser can start a scroll without waiting for that listener.
Non-passive listenerMay call preventDefault() on a cancelable event.A scrollable area may need main-thread confirmation first.
Coalesced pointA real earlier point merged into a delivered pointer event.Useful when a drawing path needs more detail.
Predicted pointAn estimated future position based on movement so far.Useful only as a temporary guess that later real input can correct.
Sort the input decisions
  • A wheel listener registered with { passive: true }.
  • A scrollable area with no potentially canceling listener.
  • A non-passive touch listener that might call preventDefault().
  • A potentially canceling touch listener high on the page.
  • Earlier pointer positions from getCoalescedEvents().
  • Estimated future positions from getPredictedEvents().
Try it yourself
0 of 6 correct

Sort each card by whether scrolling can begin, main-thread confirmation is needed, or it is extra pointer data.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Passive means no handler runs.” The handler runs normally; it simply cannot cancel the default action.
  • “All scrolling waits for JavaScript.” Browsers can often scroll on the compositor when they have no cancellation reason to wait.
  • “A cover always receives the event.” Hit testing skips a cover with pointer-events: none.
  • “Predicted points happened already.” They are estimates and must not replace actual input history.
Correct the common input myths
MythWhat is truePractical response
Passive means the handler does not run.The handler still runs; it only gives up cancellation.Use passive for observation such as analytics or visual updates.
Every wheel event waits for JavaScript.Potentially canceling listeners can require waiting; passive listeners let the browser proceed.Browser defaults also vary by target and event, so state the option explicitly.
pointer-events: none disables all pointer events.It removes that element from hit testing; another element may become the target.It is often useful for a visual overlay that should not block a button.
Predicted points are facts.They are browser estimates and can differ from later actual input.Draw them separately and be ready to correct the result.

Practice exercises

Exercise 1 · Warm-upPredict the passive output

Type the program's output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const passive = true;
console.log(passive ? "scroll now" : "wait");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the cancellation check

    Which property tells you whether cancellation can matter?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the element under a point

      Name the API used to find the element under a viewport point.

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeLet an overlay stop blocking hits

        What CSS value should a purely visual cover use?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose precise drawing points

          Which method returns earlier pointer positions merged into one delivery?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const points = [10, 15, 20];
          console.log(points.length);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real analytics listener

            Your store records wheel activity but never cancels it. What option should its listener declare?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, separate browser routing, cancellation permission, target selection, and the fidelity of pointer data.

              Input pipeline quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does { passive: true } promise?

                Choose an answer to see the explanation.

              2. Question 2 of 6What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const passive = true;
                console.log(passive ? "scroll now" : "wait");

                Choose an answer to see the explanation.

              3. Question 3 of 6Why can a non-passive wheel or touch listener delay scroll start?

                Choose an answer to see the explanation.

              4. Question 4 of 6What does document.elementFromPoint(40, 40) return?

                Choose an answer to see the explanation.

              5. Question 5 of 6When are coalesced pointer samples useful?

                Choose an answer to see the explanation.

              6. Question 6 of 6What is honest handling of predicted pointer events?

                Choose an answer to see the explanation.

              Key takeaways

              • The compositor can keep a scroll smooth when it has no reason to wait for a potentially canceling listener.
              • A non-fast scrollable region represents work that may need main-thread confirmation before scroll starts.
              • Passive means “I will not cancel”; preventDefault() in that listener is ignored.
              • Hit testing finds the element beneath a coordinate, and pointer-events: none removes a visual cover from that search.
              • Coalesced points are earlier real movement; predicted points are optional estimates.

              Remember the one-liner.
              Input stays smooth when the browser knows it does not need to wait for your JavaScript.

              Coming next: Bindings: how DOM objects exist in JavaScript.

              CompleteFrontend Clear concepts. Working examples.