Published 15 August 2026 · Last updated 15 August 2026
A web page never sees a key being pressed. It sees a KeyboardEvent object
that the browser has already built, queued and dispatched. Everything measured on this site
lives in the gap between two moments: when the browser stamped that event, and when our
handler actually ran.
That gap is small, real and reproducible. It is also the only segment of the input chain
a browser can measure without guessing. This guide walks the pipeline, explains what
KeyboardEvent.timeStamp represents, and shows why the way a handler is written
changes the number it reports.
The path from key press to handler
A single keystroke passes through several independent systems before any JavaScript runs. Roughly, in order:
- The switch closes. The keyboard's controller notices on its next scan of the key matrix, once its debounce filter is satisfied.
- The controller builds a report describing which keys are down and waits for its next transmission opportunity — a poll from the USB host, or a radio window on a wireless link.
- The operating system's input stack receives the report, applies the keyboard layout, runs it past any driver or remapping software, and routes it to the focused window.
- The browser receives the key from the OS. In current mainstream browsers the tab's content runs in a separate process from the browser's own UI, so the input is passed across a process boundary to the renderer for your tab.
- The renderer creates the
KeyboardEventand stamps itstimeStamp. - Dispatching that event is a task for the page's main thread. If the main thread is busy, the event waits in the queue — tasks are not interrupted mid-run.
- The event is dispatched: capture phase down the tree to the target, then bubble phase back up. Every registered listener runs in order, on the same thread.
- Our handler runs and reads the clock.
Steps 1 to 3 happen entirely outside the browser. No information about their duration travels with the event, which is the single most important fact about browser-based keyboard measurement — see why browser tests can't measure hardware latency for the full argument.
What KeyboardEvent.timeStamp actually is
In modern browsers, Event.timeStamp is a DOMHighResTimeStamp:
a value in milliseconds, measured from the page's time origin, on the same monotonic clock
that performance.now() reads. Because the two share a timeline, you can subtract
one from the other directly and get a meaningful duration.
Three properties matter:
- It is monotonic, not wall-clock. It does not jump when the system clock is corrected, so a time-sync event cannot corrupt a measurement.
- It is stamped at event creation — the earliest browser-side moment available. It is not a hardware timestamp and does not describe when the switch closed.
- It is coarsened like every other high-resolution clock in the page. Browsers deliberately reduce timer precision as a side-channel defence, so the smallest difference the value can express depends on the browser and its configuration.
Some engines and configurations have historically handed back an epoch-scale value
instead — a number near 1.7 × 1012 rather than a few thousand. Our
timing module normalises before doing any arithmetic: anything above 1012 has
performance.timeOrigin subtracted from it, and anything that is not a finite
positive number is rejected outright so the sample is excluded rather than silently turned
into nonsense.
The measurement
The flagship number on this site is the browser input event latency:
latency = performance.now() // read FIRST inside the keydown handler
− normalizeEventTimestamp(ev) // the event's own timestamp
That delta covers event creation, the wait in the main-thread task queue, dispatch through the DOM tree, and any listener that ran before ours. It covers nothing before the browser built the event and nothing after our handler returns. Three related channels are collected from the same events:
| Channel | Formula | What it describes |
|---|---|---|
| Event latency | handlerNow − ev.timeStamp | Browser input event latency — queue wait plus dispatch |
| Input-to-frame | frameNow − ev.timeStamp | Browser-side time until the next animation frame begins |
| Event interval | ev[i].timeStamp − ev[i−1].timeStamp | Browser-observed gap between consecutive events |
keydown, keypress, keyup and repeat
keydown fires when a key goes down and carries
key, code, location and the modifier flags. It is the
right event to time, because it is the first one the browser produces for a press.
keypress is deprecated and should not be used in new code.
It was only ever fired for character-producing keys, its behaviour differed across engines,
and it is not part of how modern browsers are expected to report text input. If you need the
text a key produces, listen for beforeinput or input instead.
keyup fires on release. Its latency can be measured the same
way, but a release is rarely the interesting moment; it is more useful for hold time and for
spotting keys that never report a release, which is what the
stuck key test looks for.
Two event properties decide whether a sample counts at all:
event.repeatistruewhen the event is auto-repeat generated by the operating system while a key is held. It is not a second physical press, so it is routed to the repeat channel and excluded from press latency.event.isTrustedisfalsefor events created by script and delivered withdispatchEvent(). Those get a timestamp at dispatch time, so their apparent latency is near zero and would quietly drag every statistic down. They are excluded.
If you want to see all of these fields for real keystrokes, the keyboard event tester prints them as they arrive.
Why reading the clock first matters
Everything that executes before the clock read is counted as latency. Fetching
ev.key, calling document.querySelector(), logging to the console,
or reading a layout-dependent property all take time, and that time lands inside your
measurement rather than outside it. The clock read has to be the first statement in the
handler:
el.addEventListener('keydown', function (ev) {
var t0 = performance.now(); // nothing above this line
// validation, storage and rendering all happen after
}, { capture: true });
The listener is registered in the capture phase for the same reason. Capture listeners run on the way down the tree, before listeners attached to the target or its ancestors in the bubble phase, so another script's handler cannot add its own execution time to our number first.
How main-thread work inflates the number
JavaScript in a page runs on one thread, and a running task is not preempted. If a long task is executing when your keystroke arrives, the event sits in the queue until that task finishes. A 30 ms layout recalculation happening at the wrong instant produces a 30 ms sample, and the keyboard had nothing to do with it.
Common sources of that delay: large DOM updates, forced synchronous layout, parsing or stringifying big objects, garbage collection, animation callbacks doing real work, other listeners on the same event, and browser extension content scripts injected into the page.
This is why the measurement handler here is kept nearly empty. It reads the clock,
validates the event against the exclusion rules, pushes a small object onto an array, and
returns. Nothing is formatted, nothing is drawn, and no layout is read. The visible readouts
are updated inside a requestAnimationFrame callback at most once per frame, and
charts are drawn after the run ends. A test page that redrew a chart on every keystroke would
be measuring its own rendering cost.
The practical consequence for anyone reading a result: a median that suddenly rises, or a 95th percentile far above the median, is usually a statement about how busy the machine and the page were, not about the keyboard. Why results vary between systems covers the rest of those variables.
Input-to-frame is a different question
The second channel schedules a requestAnimationFrame callback from inside the
handler and takes its timestamp. That callback runs just before the frame is composited, so
the value is a lower bound on when pixels could have changed — it still excludes compositing,
scan-out and the display's own response.
It is also quantised by your refresh rate. At 60 Hz frames are 16.7 ms apart, so an event arriving at a random point in the cycle waits about 8.3 ms on average and up to 16.7 ms in the worst case. At 144 Hz that becomes a 6.9 ms frame, at 240 Hz a 4.2 ms frame. Mixing this channel with event latency in one average would hide that structure entirely, which is why they are reported separately.
What the number is, and what it is not
It is a browser-observed measurement of the software path between event creation and handler execution, on this machine, in this browser, right now. It is reproducible enough to compare two sessions on the same setup, and sensitive enough to reveal a busy main thread or a misbehaving extension.
It is not your keyboard's hardware latency, not a switch measurement, and not a transport measurement. No browser API exposes those. The measurement methodology states the formulas, the validation rules and the limits in full, and you can watch the numbers appear for yourself on the keyboard latency test.