A few weeks ago I wanted to check my browser frame rate
quickly without downloading any software. Every tool I
found was either a desktop app, required an account, or
was buried behind ads.
So I built fpstest.pro, a free browser-based FPS tester
that works instantly with zero setup.
Here is how the core FPS measurement works and what I
learned building it.
The Core Problem: Measuring Real FPS in a Browser
The browser does not expose a direct "current FPS" API.
You have to calculate it yourself using
requestAnimationFrame.
The idea is simple. requestAnimationFrame calls your
callback once per frame. If you track the timestamps
between calls, you can calculate how many frames are
rendering per second.
let frames: number[] = []
let rafId: number
function tick(timestamp: number) {
frames.push(timestamp)
// Keep only last 60 frames for rolling average
if (frames.length > 60) {
frames.shift()
}
rafId = requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
Every 500ms I calculate the current FPS from the frame
timestamps:
function calculateFPS(frames: number[]): number {
if (frames.length < 2) return 0
const elapsed = frames[frames.length - 1] - frames[0]
const fps = ((frames.length - 1) / elapsed) * 1000
return Math.round(fps)
}
The Stats: Avg, Min, Max, Frame Time, Stability
Just showing current FPS is not enough. I wanted the
same stats you see in MSI Afterburner.
Average FPS is straightforward — sum all calculated
FPS readings and divide by count.
Min and Max track the lowest and highest FPS
recorded during the test.
Frame Time is 1000 / currentFPS — how long each
frame takes in milliseconds. At 60 FPS this is 16.7ms.
At 144 FPS it drops to 6.9ms.
Stability is the interesting one. I calculate the
standard deviation of frame deltas and convert to a
percentage:
function calculateStability(frames: number[]): number {
if (frames.length < 10) return 100
// Get frame deltas (time between each frame)
const deltas = []
for (let i = 1; i < frames.length; i++) {
deltas.push(frames[i] - frames[i - 1])
}
const mean = deltas.reduce((a, b) => a + b, 0) / deltas.length
const variance = deltas.reduce((a, b) =>
a + Math.pow(b - mean, 2), 0) / deltas.length
const stdDev = Math.sqrt(variance)
// Lower stdDev = more stable
const stability = Math.max(0, 100 - (stdDev / mean * 100))
return Math.round(stability)
}
100% stability means perfectly consistent frame timing.
In practice 95%+ is excellent, below 80% means noticeable
stutter.
The Cleanup Problem
This caught me early. If the user navigates away or the
component unmounts while the rAF loop is running, you
get a memory leak and potential state updates on an
unmounted component.
Always cancel on cleanup:
useEffect(() => {
if (gameState !== 'running') return
rafId = requestAnimationFrame(tick)
// Cleanup: cancel rAF when component unmounts
// or when gameState changes
return () => {
cancelAnimationFrame(rafId)
}
}, [gameState])
The Monitor Hz Detector
This is a simpler version of the same idea. Instead of
tracking FPS over 10 seconds, I count raw frames over
exactly 3 seconds:
async function detectHz(): Promise<number> {
return new Promise((resolve) => {
let frameCount = 0
const startTime = performance.now()
function count() {
frameCount++
if (performance.now() - startTime < 3000) {
requestAnimationFrame(count)
} else {
const hz = frameCount / 3
resolve(roundToStandard(hz))
}
}
requestAnimationFrame(count)
})
}
function roundToStandard(hz: number): number {
const standards = [60, 75, 90, 120, 144, 165, 240, 360]
return standards.reduce((prev, curr) =>
Math.abs(curr - hz) < Math.abs(prev - hz) ? curr : prev
)
}
The roundToStandard function snaps the raw measurement
to the nearest real monitor spec. A reading of 143.7Hz
rounds to 144Hz.
One Gotcha: Battery Mode
I noticed during testing that laptops on battery saver
mode consistently showed 30-40 FPS even on a 144Hz
display. The browser throttles requestAnimationFrame
when the system is in power saving mode.
Added a note in the UI: "For accurate results, plug in
your laptop."
What I Used
- Next.js 15 App Router with TypeScript
- Tailwind CSS for styling
- Recharts for the live FPS graph on the homepage
- Lucide React for icons
- Zero external APIs — everything runs in the browser
The Result
The site is live at fpstest.pro with 6 free tools:
- FPS Meter (the one above)
- UFO Motion Test (see 30/60/120/144 FPS visually)
- Frame Rate Comparison (side by side)
- FPS Reaction Test
- Monitor Hz Detector
- Input Lag Test
Also built a Chrome extension version that puts the FPS
meter, Hz detector, and reaction test in a popup —
submitted to the Chrome Web Store yesterday.
The requestAnimationFrame Accuracy Question
One thing worth knowing: browser rAF is not perfectly
accurate for FPS measurement. The browser can skip or
delay callbacks under heavy load, which is actually
useful for our purposes — we want to measure real
rendering performance, not theoretical maximum.
For a simple canvas test like this, rAF gives you a
good signal for whether your system is struggling. It
will not match MSI Afterburner numbers for in-game FPS,
but that is a different measurement entirely.
If you are into browser performance or gaming tools,
check out fpstest — all free, no signup, works
on any device.
Happy to answer any questions about the rAF
implementation in the comments.
Top comments (0)