reaction test.
A scrolling note. A line. A window measured in frames at your monitor's actual refresh rate. Click when they overlap. Below the tool, the bits explained — and the honest story about what your browser can and can't measure.
click on the beat
press start
why this works at sub-frame precision
The browser can't paint anything in between display refreshes — that's a hardware limit. Your monitor refreshes once every 1000 / refreshRate ms (16.67 ms at 60 Hz, 4.17 ms at 240 Hz), and requestAnimationFrame fires once per refresh. So a "frame-perfect" visual cue can only appear on those frame boundaries.
But measuring when you clicked is a different problem. Thepointerdown event carries a timeStamp inDOMHighResTimeStamp format — sub-millisecond precision, from the same clock as performance.now(). The cue's paint time comes from the requestAnimationFrame callback's argument, which is the timestamp of that frame's scheduled paint. The hit-window check is just |clickTime − cueTime| ≤ frames × frameMs. The math is sub-frame even though the visual is not.
Caveat: some browsers reduce performance.now() precision to ~100 µs as a fingerprinting defence, and a few report the event-dispatch time rather than the hardware time, undercounting your reaction by a millisecond or two. Still well below a single frame.
what WebHID would change (and what it wouldn't)
WebHID lets a webpage talk directly to USB HID devices, including your mouse. With user permission and HTTPS, the page reads raw HID input reports at the mouse's polling rate (1000 Hz on a gaming mouse) and gets timestamps before the OS turns them into pointer events.
The realistic gain over pointerdown is about 1–2 ms on USB and zero (or worse) on Bluetooth. Your monitor refresh, mouse polling rate, and physical reflex dominate the latency budget by 10–50× over what WebHID saves. Plus, WebHID is Chromium-only and requires a permission picker on first use. Not worth the friction here.
The architecture leaves the door open: input is consumed via an InputSource interface, and the v1 implementation isPointerInputSource. Adding a WebHIDInputSource later is a one-class addition with no game-loop changes.
how this is built
- Refresh-rate detection. Sample 30 rAF deltas, take the median (robust to GC pauses), invert to Hz, and snap to the nearest known rate within ±4 Hz. Shown above so you can sanity-check your hardware.
- Game loop. A single
requestAnimationFramethat computes note position fromnow − spawnedAtrather than accumulating per-frame deltas — so a tab-pause doesn't drift the timing. - Note rendering. One
divupdated each frame viastyle.transform = translate3d(...), written through a ref. No React re-render per frame. - Input.
pointerdown+keydownonwindow, both with sub-millisecondtimeStamp. Press anywhere on the page (or hit any key) — the lane is just a visual target. - Streak persistence. Best streak per (refresh rate, frame count) pair in
localStorage, so 3 frames at 144 Hz is its own leaderboard slot.