toys

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

detecting refresh rate…
streak 0best 0
frames (window in your monitor's frames)
5 / 15

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.

Hit window timingcue paints (T₀)click (T₁)± window/2

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 requestAnimationFrame that computes note position from now − spawnedAt rather than accumulating per-frame deltas — so a tab-pause doesn't drift the timing.
  • Note rendering. One div updated each frame via style.transform = translate3d(...), written through a ref. No React re-render per frame.
  • Input. pointerdown + keydown on window, both with sub-millisecond timeStamp. 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.

← back to toys