DEV Community

Cover image for Measuring reaction time in the browser is harder than it looks
Daniel Pertu
Daniel Pertu

Posted on AI-assisted

Measuring reaction time in the browser is harder than it looks

If your app measures how fast someone reacts to something, the number you record is only as good as your timing. I learned this the hard way building game-based cognitive assessments, where the reaction time basically is the measurement — a sloppy 40ms of jitter turns a real signal into noise.

Here's what actually matters.

Use performance.now(), not Date.now(). Date.now() is wall-clock time — it can jump around when the system clock syncs, and its resolution is coarse. performance.now() is monotonic and high-resolution, measured from a fixed time origin. Everything below assumes it.

Don't timestamp inside your event handler. The intuitive approach is: when the click fires, call performance.now(). But your handler might run late if the main thread is busy, and you'd be measuring "when my JS got scheduled," not "when the user acted." Every modern input event already carries an accurate event.timeStamp from the same time origin:

button.addEventListener("pointerdown", (e) => {
  const reactionMs = e.timeStamp - stimulusShownAt;
});
Enter fullscreen mode Exit fullscreen mode

Timestamp the stimulus at paint, not at state-set. When you call setState to show the target, the pixels don't hit the screen on that line — they hit after the next composite. The closest honest "shown at" is the requestAnimationFrame callback timestamp for the frame that actually rendered it:

requestAnimationFrame((frameTime) => {
  stimulusShownAt = frameTime; // when the browser is about to paint
});
Enter fullscreen mode Exit fullscreen mode

Accept the limits, then design around them. You still can't beat the display: a 60Hz screen only updates every ~16.7ms, browsers coarsen timers to mitigate Spectre-style attacks, and background tabs get throttled. So I stopped pretending to measure absolute lab-grade milliseconds and instead:

  • measured relative differences within a session, where the constant offsets cancel out,
  • flagged responses that arrived before the stimulus (anticipations/guesses) and threw them out,
  • normalised against a per-user baseline captured in a warm-up round. The takeaway that generalises: for any timing-sensitive UI, separate "when did the thing appear" from "when did the user act," get both from the browser's own high-res clock, and be honest about the noise floor you can't remove.

I'm building CogniPrep, a practice platform for game-based psychometric assessments — this reaction-timing stuff is the guts of it. Link: https://cogniprep.app

Top comments (0)