Measurement Methodology

Every formula, every exclusion rule and every limit of the timing measurements on this site — written so you can check our work.

Published 15 August 2026 · Last updated 15 August 2026

The short version

This site measures how long a keystroke takes to move through your browser: from the moment the browser creates the keyboard event, to the moment our JavaScript runs, to the next painted frame. That is a real, reproducible measurement of the browser input pipeline and main-thread scheduling. It is not your keyboard's hardware latency, and nothing measured inside a web page can be.

1. What we measure

Every timing test here uses one shared collector, so these rules apply identically on the keyboard latency test, the performance test and every diagnostic page. It records three quantities, which answer three different questions.

The three measurement channels and their exact formulas.
ChannelFormulaWhat it is called
A — Event latency handlerNow − eventTimestamp Browser input event latency
B — Input-to-frame frameNow − eventTimestamp Input-to-frame time (browser-side)
C — Event interval eventTimestamp[i] − eventTimestamp[i−1] Browser-observed event interval

Channel A — browser input event latency

The flagship number. performance.now() is read as the very first statement inside the keydown handler, which is registered in the capture phase on the window so it runs before any other key handler on the page can add its own delay. The other half of the subtraction is KeyboardEvent.timeStamp, stamped when the browser creates the event — shortly after the OS hands the input to the browser process. Both sit on the same monotonic clock.

It includes event creation, queueing and the wait for a free main thread, so a busy tab, a long layout pass or a garbage-collection pause all show up here. It excludes everything before the browser saw the input — switch, debounce, firmware, USB or Bluetooth transport, driver, OS queue — and everything after the handler returns.

Channel B — input-to-frame

From the same handler we schedule a requestAnimationFrame callback and read the timestamp the browser passes it. That callback runs immediately before the frame is composited, so channel B is a lower bound on when pixels changed: browser-side frame timing, never display response time.

Refresh rate sets the floor. Frames arrive every 16.7 ms at 60 Hz, 6.9 ms at 144 Hz and 4.2 ms at 240 Hz, and a press landing at a random point in that cycle waits half a frame on average before the browser can draw at all.

Channel C — browser-observed event interval

The gap between two consecutive accepted event timestamps, reported by the event cadence test. It is deliberately not called a polling rate: a browser only sees the events the OS chose to deliver, never the USB transfers underneath. With a key held down these intervals reflect your operating system's key-repeat setting, which is what the repeat rate test measures.

The 250 ms fence used on the latency channels is not applied here — a 300 ms gap between deliberate presses is just normal typing — and the first press of a run produces no interval at all, so it is counted as a reference event rather than an exclusion.

2. The latency chain

A keystroke passes through around nine distinct stages between your finger and the photons leaving your screen. A web page can observe three of them.

The keyboard latency chain, from switch actuation to photons A nine-stage vertical chain. The first five stages — switch actuation, keyboard matrix scan and debounce, firmware building an HID report, USB or Bluetooth transmission, and the operating system input stack — all happen before a web page can observe anything. The three highlighted stages in the middle are the browser creating the keyboard event, the handler running in the page, and the next frame becoming ready to paint; only these three are measured on this site. The final stage, the display changing its pixels, is again invisible to a web page. Before the browser — not measurable Switch actuation and key travel Matrix scan and debounce Firmware builds an HID report USB or Bluetooth transmission OS input stack and scheduler Browser-observable — measured here Browser creates the event start Handler runs in the page A ends Next frame is ready to paint B ends After the browser — not measurable Display response, pixels change
The same chain as text, with what each stage contributes.
StageWhat happensVisible to a web page?
Switch actuationThe key travels far enough for the switch to register a contact.No
Matrix scan, debounceThe controller sweeps the key matrix on its own cycle and waits out contact bounce.No
FirmwareThe state change becomes a report in the format described by the USB HID specification.No
TransportThe report crosses USB, Bluetooth or a proprietary 2.4 GHz link.No
Operating systemA driver receives the report; the OS routes it to the focused window and queues it.No
Browser event creationThe browser builds a KeyboardEvent and stamps it with a high-resolution timestamp.Yes — our start point
Dispatch to JavaScriptThe event waits for the main thread, then our handler runs.Yes — channel A
RenderStyle, layout and paint work runs; the frame goes to the compositor.Yes — channel B
DisplayThe panel scans out the frame and its pixels physically change.No

For an end-to-end estimate rather than a measurement, the latency calculator lets you put your own figure on each greyed-out stage and shows the arithmetic. Every value in it is an assumption you supply.

3. Timestamp normalization

Before any subtraction, the event timestamp is checked and, if necessary, converted. Skipping this step is the easiest way to produce a nonsense latency figure.

A DOMHighResTimeStamp counts milliseconds since the page's time origin, so it stays small. Some engines and configurations have historically handed back an epoch-scale value instead — the Date.now() scale, currently around 1.7 × 1012 — and subtracting that from performance.now() yields a latency of roughly minus fifty-four years.

if (typeof ts !== 'number' || !isFinite(ts) || ts <= 0) return null;   // unusable
if (ts > 1e12) return ts - performance.timeOrigin;                    // epoch scale
return ts;                                                            // already high-res

The 1012 ms threshold is about 31.7 years of page uptime, so no genuine high-resolution timestamp can reach it while every epoch timestamp sits far above it — the two cases cannot be confused. The explicit typeof check is not paranoia: both '123' > 0 and Infinity > 0 are true in JavaScript, so a bare positivity test would let a string or an infinity poison every statistic downstream. Anything that fails returns null and surfaces as a visible bad-timestamp exclusion rather than a silent corruption.

4. Timer resolution

Browsers deliberately blunt their own clocks. High-resolution timers were one ingredient in the Spectre and Meltdown class of side-channel attacks published in 2018: time an operation precisely enough and a page can infer things it was never meant to see. Every major engine responded by reducing the precision of performance.now().

The exact clamp depends on the browser and on how the page is served — Firefox has for some time rounded to 1 millisecond unless the page is cross-origin isolated, while Chromium-based browsers typically coarsen to a much finer step, on the order of 100 microseconds. Rather than assume any of that, the site measures it: performance.now() is called twice in a row, thousands of times, keeping the smallest non-zero difference it ever sees.

a = performance.now();
b = performance.now();
d = b - a;            // keep the smallest d > 0

The loop is bounded three ways so it can never stall a page load: 20,000 iterations, a wall-clock budget of about 8 ms, and an early exit once the minimum has stopped improving for sixteen consecutive non-zero readings. The result is rounded to the nanosecond to strip floating-point noise, cached, and shown in every page's Measurement Environment panel.

What a coarse clock means for your result

Plainly: on a browser clamped to 1 ms, sub-millisecond jitter simply cannot be observed. Every latency becomes a whole number of milliseconds, a genuine 0.4 ms difference between two setups is invisible, and jitter and standard deviation look artificially clean — sometimes exactly zero — because quantisation, not your keyboard, produced those tidy numbers. When the probe reports 1 ms or worse the page says so beside your results, and the test quality drops accordingly.

Resolution is not accuracy, either. Read the figure as the floor on what is visible, not as an error bar — see the caveat in section 11.

Cross-origin isolation

A page becomes cross-origin isolated when served with the COOP and COEP response headers, which assure the browser that no cross-origin content shares its process. Browsers grant such pages the finer timers withdrawn from ordinary pages, because the side-channel risk is contained; the trade-off is that an isolated page may not casually embed cross-origin resources. The environment panel reports the state your browser reports for this page, so you never have to take our word for which clock you got.

5. Sample validation

A key event becomes a latency sample only if it passes every check below. Anything that fails is recorded with a reason code — never discarded quietly.

The complete exclusion table. Checks run in this order.
Reason codeConditionWhy it is excluded
untrusted event.isTrusted !== true Generated by script, not by a device. Its timestamp describes the script, not an input.
repeat event.repeat === true Auto-repeat events are manufactured by the OS on a timer. Excluded from press latency and routed to the repeat channel, where they are genuinely informative.
no-focus document.hasFocus() === false Without focus the browser may deprioritise the page, so the delay measured reflects the window manager rather than input handling.
hidden document.visibilityState !== 'visible' Hidden tabs are heavily throttled by design. Timing collected there is meaningless.
bad-timestamp Normalization returned null The timestamp was missing, zero, negative, infinite or not a number.
negative latency < 0 The handler appears to have run before the event existed — a clock anomaly, not a fast keyboard.
outlier-high latency > 250 ms Almost always a page stall or system hiccup rather than input timing. Counted separately so the count stays visible.
modifier-only Bare Shift, Ctrl, Alt, AltGr or Meta A real event with real latency, but not "a keypress" in the sense these tools report. Still logged.
filtered A tool-specific rule rejected the key Used where a test only cares about certain keys, such as a prompted key. Reported like any other exclusion.

Details that matter

The order is not arbitrary. modifier-only is checked before the value fences, so a slow bare Shift can never be filed as an outlier-high and misread as a sluggish keypress. The two value fences necessarily run last, because they need the computed value.

Lock keys are deliberately not treated as modifiers: Caps Lock, Num Lock and Scroll Lock are ordinary keys that happen to toggle a state, and excluding them would distort the coverage figures on the keyboard tester. Losing focus and switching tabs count as one interruption, not two, even though the browser fires two separate events for it.

Exclusions are always reported

This is a rule, not a preference. Every result panel renders a sentence of the form "3 samples excluded — 2 key-repeat, 1 window lost focus", backed by a disclosure that lists each excluded event with its key, its reason and its timestamp. A median shown without the count of what was thrown away to produce it is a bug, not a tidier result.

6. Statistics

Every statistic comes from one shared implementation with one definition each. Anything that cannot honestly be computed returns nothing rather than a number.

Percentiles and the median

Percentiles use linear interpolation between order statistics — the default method of numpy.percentile and of Excel's PERCENTILE.INC. With the valid samples sorted ascending as x, zero-indexed, length n:

rank = (n - 1) * p / 100
lo   = floor(rank),  hi = ceil(rank),  f = rank - lo
P    = x[lo] + f * (x[hi] - x[lo])

So p = 0 gives the minimum, p = 100 the maximum, and p = 50 on an even-length sample the mean of the two middle values — the ordinary median. We avoid the "nearest rank" method, which snaps a percentile onto an actually-observed sample and overstates how much the data supports it.

Why the median leads

Latency distributions are right-skewed. A press is never faster than the pipeline allows, but any press can be arbitrarily slower if a background task or a compositor hiccup lands on it — and one 180 ms sample in a run of thirty drags the mean up noticeably while barely moving the median.

That makes the median the better answer to "what happens on a typical keypress". The mean is still shown, because the gap between the two is itself a direct sign of a heavy tail, and the percentiles then say how heavy.

Spread: standard deviation, IQR and CV

The standard deviation is the sample standard deviation, denominator n − 1. Your thirty presses are a sample from a much larger population of possible presses, and dividing by n would systematically understate that population's spread; at n = 10 the correction makes the reported figure about 5% larger.

The interquartile range is P75 − P25, the width of the middle half, which no single extreme value can move. The coefficient of variation is stdev / mean, shown as a percentage — that is what lets a 4 ms setup and a 12 ms setup be compared on consistency at all. It is reported as unavailable, never as zero, when the mean is not positive.

Jitter is not the standard deviation

Jitter here is the mean absolute successive difference: the average size of the step from one sample to the next, in collection order. We name the statistic rather than attributing it to a standard: definitions of “jitter” differ between fields, and the interarrival jitter used in real-time networking is a smoothed running estimate rather than the plain average used here.

jitter = mean( |x[i] - x[i-1]| )   for i = 1 .. n-1

It is labelled "Jitter (mean successive difference)" wherever it appears, because calling a standard deviation "jitter" throws away what makes jitter useful. Take two runs built from exactly the same six values — Run A of 5, 10, 5, 10, 5, 10 ms, alternating every press, and Run B of 5, 5, 5, 10, 10, 10 ms, one clean shift halfway through.

Both have a mean of 7.5 ms and a standard deviation of about 2.74 ms, because neither statistic knows anything about order. Jitter does: Run A scores 5.0 ms and Run B scores 1.0 ms. A is erratic press to press; B was stable and then something changed. That is exactly the difference a typist feels, so the samples are never sorted first.

Outliers, and the P95 / P99 gates

Outliers use the Tukey fence, [P25 − 1.5·IQR, P75 + 1.5·IQR]. They are flagged and counted, not deleted — the headline statistics still include them, and the only samples that leave the dataset are those that failed a rule in section 5. Below four samples the fence is not computed, since it could not exclude anything anyway.

P90 appears as soon as there is data. P95 needs at least 10 valid samples and P99 at least 20; below that the site renders an em dash and says what it needs. The formula shows why: at n = 10, P99 has rank = 9 × 0.99 = 8.91, placing it 91% of the way from the second-largest sample to the largest. That is not a tail estimate, it is the maximum wearing a label. To say anything about the worst 1% of presses, you need enough presses for 1% to contain something.

7. Consistency bands and test quality

Two labels appear alongside results. Both are mechanical functions of numbers you can see, and both are published here so a label can never mean something the page did not tell you.

Consistency

Derived from the coefficient of variation, and always rendered next to the number that produced it — Consistent (CV 22%), never the word alone. A label by itself would be unfalsifiable, and a colour by itself would be invisible to a great many readers.

Consistency bands. Thresholds are inclusive at the lower edge.
Coefficient of variationLabel
Below 0.15Very consistent
0.15 to 0.30Consistent
0.30 to 0.50Moderate variation
0.50 and aboveHigh variation

Test quality

This label describes the test you just ran, not your keyboard: how many valid samples you collected, what fraction of events were excluded, how often the window lost focus, and the timer resolution the probe found.

Test quality thresholds, evaluated top to bottom.
ConditionQuality
Fewer than 10 valid samplesInsufficient — collect more samples
30+ valid, under 10% excluded, no focus losses, timer resolution under 1 msExcellent
20+ valid, under 20% excluded, at most 1 focus lossGood
Anything else with 10 or more valid samplesLimited

The exclusion fraction is measured over every key event the session saw, so it is excluded / (valid + excluded). Whenever the label is anything other than Excellent, the page also lists the specific reasons, so you can fix the run rather than guess.

8. The Browser Test Profile score

Exactly one page produces a single 0–100 number: the keyboard performance test. It is called a Browser Test Profile, and the name is chosen carefully — it scores how your keyboard, your operating system and your browser behaved together during that run, in that tab. It is not a keyboard rating, it is not comparable across machines, and it would move if you closed a few background tabs.

The complete weighting. These are the only inputs to the score.
ComponentWeightWhat it reflects
Event latency median30Typical browser-observed delay from event creation to handler.
Consistency (CV)25How much that delay varies relative to its own size.
Key detection coverage20How many of the keys you pressed registered correctly.
No chatter15Absence of suspiciously fast duplicate events for one press.
Input-to-frame10Browser-side time from event to the next painted frame.

The same table is printed on the performance test page. If you would rather compare two configurations than reduce either to a number, the session comparison tool puts two saved runs side by side with their full statistics instead.

9. What we cannot measure

The honest boundary

No web page can observe what happens before the browser receives an event, or after it hands a frame to the compositor. Everything below is outside what any browser-based test can reach — including this one.

  • Switch actuation and debounce. Needs an oscilloscope or logic analyser on the switch contact, or a high-speed camera watching the keycap.
  • Firmware scan-to-report time. Needs instrumented firmware toggling a spare pin at the scan and again at the report, read by a logic analyser.
  • Transport time and the real polling rate. Needs a USB protocol analyser, or a logic analyser on the data lines, watching the transfers themselves.
  • Operating system input latency. Needs kernel-level tracing tools on the platform in question, not JavaScript.
  • Display response and total click-to-photon latency. Needs a high-speed camera or a photodiode aimed at the screen, timestamped against an external trigger on the key press. That is how genuine end-to-end input-lag figures are produced.
  • Which stage caused a slow result. We can say the browser-side delay was high; we cannot say whether the cause was the keyboard, a driver, a background process or the page itself.
  • Anything comparable across two computers. Two results from two machines differ by browser and system load at least as much as by keyboard.

The guide on why browser tests cannot measure hardware latency works through each of these in more detail.

10. Why results vary

Running the same test twice on the same keyboard can produce visibly different numbers. That is not instability in the measurement; it is the measurement correctly reporting that conditions changed.

  • Browser engine. Engines create, queue and dispatch events differently and clamp their clocks differently, so Chrome and Firefox results on one machine are not directly comparable.
  • Operating system. Same hardware, different input stack, different numbers.
  • Background load. Anything competing for the main thread widens the distribution, usually as a heavier tail rather than a higher median.
  • Power profile. Battery saver, thermal throttling and frequency scaling lengthen the time a sleeping core takes to wake and process an event, so laptops on battery frequently measure worse than the same laptop plugged in.
  • Focus and tab state. Background and hidden tabs are throttled by design; those samples are excluded outright.
  • Display refresh rate. Channel B is bounded below by the frame interval, so 60 Hz and 240 Hz displays report different input-to-frame figures for identical browser behaviour. Channel A is unaffected.
  • Virtual machines. Timer virtualisation and host scheduling add delay and noise unrelated to your keyboard.
  • Remote desktop. Over RDP, VNC, Parsec or anything similar, these measurements are meaningless: your keypress is captured on one machine, sent across a network and re-injected as synthetic input on another, so the timestamps describe the remote session's input injection rather than your keyboard.
  • Browser extensions. Content scripts share the page's main thread, so any extension listening for key events adds work between the event and our handler. A private or extension-free window shows how much they cost.
  • Developer tools. An open DevTools panel measurably changes main-thread behaviour; close it before testing.

There is a fuller treatment in why keyboard latency results vary between systems. To compare two configurations honestly: change one variable, keep everything else identical, collect at least 30 samples each, and compare medians rather than best cases.

11. What would change our mind

These are the known weaknesses in our own method. We would rather publish them than have you find them.

  1. We assume the event timestamp is stamped early. Everything rests on KeyboardEvent.timeStamp marking event creation, close to the hand-off from the OS. If a browser stamped it later — at dispatch, say — channel A would systematically under-report there, and we could not detect it from inside the page.
  2. Channel A is a lower bound, not a total. A capture-phase listener on the window is the earliest hook a page has, but the browser has already done internal work before anything of ours runs. Read it as "at least this much browser-side delay".
  3. Channel B is a lower bound too. An animation-frame callback fires before compositing, not after pixels change, and closing that gap requires a camera.
  4. The 250 ms fence is a judgement call. It is a round number chosen because values above it are almost always system stalls, not a threshold derived from data. A genuine 400 ms stall is a real event another tool might legitimately keep, which is why we always report the count the fence caught.
  5. The resolution probe measures granularity, not accuracy. If a browser adds randomised jitter on top of rounding, the probe reports the rounding step and stays silent about the jitter.
  6. There is no ground truth anywhere in this site. Every number is internally consistent and entirely uncalibrated; nothing tells us the absolute offset between what we report and what a logic analyser would say.
  7. Real sample sizes are small. Most people press twenty or thirty keys — enough for a median, thin for a tail. Hence the P95 and P99 gates.

With better tooling we would build a small reference device that presses a key and timestamps the press on an external clock, so every figure here could carry a measured offset instead of an assumption. Until that exists, the honest description of these results is "browser-observed, self-consistent, uncalibrated" — which is exactly how they are labelled. If any of the above turns out to be wrong, the code and this page get corrected together. Tell us if you find a problem with the method; it is the most useful message this site can receive.

Put the methodology to work

↑ Back to top