The input data your browser threw away

Pointer events arrive once per frame. Your trackpad samples several times faster than that, and the extra samples are still attached if you ask for them.

Pointer events are delivered once per animation frame. Your pointing device doesn't sample at that rate. A trackpad runs at 120Hz or higher, a drawing tablet at 200Hz or more, and a gaming mouse can go past 1000Hz.

The browser doesn't discard the extra samples. It merges them into the event it delivers and keeps the originals attached to it. event.getCoalescedEvents() hands them back in order, each one a full PointerEvent with its own coordinates, timestamp, pressure, and tilt.

Almost nobody calls it.

Drawing

The visible cost lands on any code that traces a path. Draw a fast curve below. The red line uses event.clientX and event.clientY, which is what nearly every drawing implementation does. The orange line iterates the coalesced samples out of the same events.

Draw fast

0 pointermove events carried 0 position samples, 0.00 per event. On a 60Hz mouse that ratio stays at 1.00 and the two paths land on top of each other.

Move slowly and you get one sample per event, so the two lines sit on top of each other. Move fast and the red line turns into a polygon while the orange one stays a curve. Same events, same handler, same frame budget. The difference is entirely in what got read out of them.

const onPointerMove = (event: React.PointerEvent) => {
  // the one merged position
  addPoint(event.clientX, event.clientY)
 
  // every position the browser actually captured
  for (const sample of event.nativeEvent.getCoalescedEvents()) {
    addPoint(sample.clientX, sample.clientY)
  }
}

Read them synchronously inside the handler. The list isn't guaranteed to stay valid after the handler returns, so copy out the values you need instead of holding onto the events.

Your hardware

The ratio depends entirely on the device, which is why this is easy to miss. The pad below measures it live.

Your device, right now

move the pointer in here

0events / secroughly your refresh rate
0samples / secyour actual device rate
0.00per eventwhat you throw away

samples per event will chart here

Your browser has no pointerrawupdate, so pointermove plus coalescing is the whole story here.

On a 60Hz mouse driving a 60Hz display the ratio is 1.00 and none of this matters. On a 120Hz trackpad against a 60Hz display it sits around 2. With a stylus it can reach 4 or higher. Whether you ever see this bug is decided by the hardware on your desk, which is a bad property for a bug to have.

pointerrawupdate is the other end of the same idea. It fires at the device rate rather than once per frame, so there's nothing to coalesce. It's Chromium only, it fires a lot, and it's worth wiring up only when you're doing something genuinely latency-critical with the data.

Prediction

The browser will also extrapolate forward. getPredictedEvents() returns positions it expects the pointer to reach next, based on recent motion.

Where the browser thinks you're going

move the pointer in here

real position predicted history

This browser has no getPredictedEvents, so the pad below shows the trail only.

Drawing apps use this to hide input latency. Render predicted ink immediately, then correct it when the real samples arrive. Watch what happens when you reverse direction quickly: the prediction overshoots, because it's extrapolating from momentum that stopped applying a few milliseconds ago.

That's why predicted points are for provisional rendering only, never for committing to a document. Chromium supports it, Firefox and Safari don't, and the detection is 'getPredictedEvents' in PointerEvent.prototype.

When it matters

Coalesced samples change the result for anything that treats pointer input as a signal rather than a position:

  • Drawing, handwriting, and signature capture
  • Gesture recognition, where the shape of the path is the input
  • Velocity for fling and inertia

They change nothing for buttons, hover states, or dragging discrete objects around. Those care where the pointer ended up, not how it got there.

Velocity is the one that usually gets missed. Computing a fling from pointermove deltas means sampling a curve at the frame rate and calling that a measurement. The samples for a better one were attached to the event the whole time.