Average FPS Is the Wrong Metric. Use P99 Frame Time.

4 min read 1 source clear_take
├── "Average FPS is a fundamentally broken metric — perceived smoothness is dominated by the worst frame, not the mean"
│  ├── Nikita Prokopov (Tonsky) (tonsky.me) → read

Tonsky argues that hitting the frame budget 99% of the time still produces one visible stutter per second, which users perceive as jank. Because a missed deadline causes the compositor to hold the previous frame for a full additional interval, a 9ms frame at 120Hz isn't 'slightly late' — it doubles in duration, creating a discontinuity the visual system latches onto.

│  └── @ravenical (Hacker News, 691 pts) → view

By submitting the post and driving it to 691 points, ravenical amplified Tonsky's core thesis that the industry has been measuring the wrong thing. The HN thread's unusual unanimity suggests broad agreement that p99 frame time, not mean FPS, is what actually correlates with perceived smoothness.

├── "The tooling ecosystem has entrenched the wrong metric because it demos well, not because it ships well"
│  └── top10.dev editorial (top10.dev) → read below

The editorial argues that Chrome DevTools, Xcode's FPS HUD, and game engine overlays all report mean FPS because '120 FPS' sells phones and GPUs while 'p99 = 19ms at an 8.33ms budget' embarrasses everyone. The HN thread's unanimity isn't about whether Tonsky is right but about why the tools optimized for marketable numbers rather than user-perceptible smoothness.

└── "Smoothness is perceptually non-additive — you cannot bank good frames against bad ones"
  └── top10.dev editorial (top10.dev) → read below

The editorial lifts out an asymmetry-of-perception point: users do not integrate frame quality over a window but latch onto the worst frame, which becomes the memorable experience. This reframes the problem from 'reduce average frame time' to 'eliminate the tail,' which has very different engineering implications than chasing higher mean FPS.

What happened

Nikita Prokopov (Tonsky) shipped 'Every Frame Perfect' last week. By the time HN had finished arguing about it, the post was sitting at 691 points — roughly double where it landed in its first 12 hours. The thesis is short enough to fit on a sticky note: a UI that hits its frame budget 99% of the time is not a smooth UI. It is a UI with one visible stutter per second.

The math is elementary and that is part of why it is rude. At 120Hz you have 8.33ms to produce a frame. At 60Hz you have 16.67ms. Miss by a single millisecond and the compositor holds the previous frame for another full interval — so a 9ms frame at 120Hz is not 'a little late,' it is doubled in duration. The user does not see 'a slow frame.' The user sees a held frame, which the visual system reads as a discontinuity in motion.

The industry measures the wrong thing: 'average FPS' is a mean of a quantity whose perceptual cost is dominated by its maximum. A scene that renders 999 frames in 8ms and one frame in 33ms reports as 119 FPS in Chrome DevTools, in Xcode's FPS HUD, in your game engine's overlay, and in every dashboard built on top of those. The user reports it as 'janky.'

Why it matters

The HN comment thread is unusually unanimous for HN. The disagreement is not whether Tonsky is right — it is about how long the industry has been measuring this wrong and why nothing changed. Several threads point at the same culprit: the tools optimized for the metric that demos well, not the metric that ships well. A bar that says '120 FPS' sells phones and GPUs. A histogram that says 'p99 = 19ms at a 8.33ms budget' sells nothing and embarrasses everyone.

The deeper point — and the one worth lifting out of the post — is the asymmetry of perception. Smoothness is not additive. You cannot bank smooth frames against future janky ones. A user watching a scroll does not integrate frame quality over the window; they latch onto the worst frame and that becomes the memory of the interaction. This is why a consistent 90Hz feels better than a 120Hz that occasionally drops to 60Hz, even though the latter delivers more frames per second on average. Apple's ProMotion engineers know this. Game engine programmers shipping competitive multiplayer know this. Web frontend has mostly not internalized it, because the web's perf tools were built around RAIL and 60 FPS as a binary pass/fail.

The Chromium team has actually been pushing in the right direction for two years — the percentDroppedFrames API and the smoothness metric in the Core Web Vitals lab tools are exactly this argument, expressed in product-management vocabulary. INP (Interaction to Next Paint) is the same idea applied to input latency: it's a p98 of interaction durations, not an average, because Google's own data showed averages hid the painful tail. Tonsky's contribution is naming the failure mode in language a senior IC can take to their team without needing a Google research paper to back them up.

There's a secondary thread in the comments worth flagging. Several commenters point out that 'every frame perfect' is what console games have always done — frame pacing in a shipped console title is a hard constraint, not a target, and the engines (Unreal, Unity, in-house) have decade-old tooling for frame-time histograms, GPU/CPU bubbles, and pacing locks. Native mobile is in the middle. The web is the laggard. The reason is structural: the web's rendering pipeline is shared between the page, the compositor, and an unknown number of extensions, and the developer does not own the frame budget end-to-end. But 'we don't fully own it' has been used to justify 'we don't measure it' for too long.

What this means for your stack

Three concrete moves, ordered by cost.

First, stop reporting average FPS in perf reviews. Replace it with a frame-time histogram and a single number: the 99th-percentile frame duration. If your refresh target is 8.33ms, your p99 budget is 8.33ms. Anything above is a regression. This is a five-minute change in your existing instrumentation if you're using `requestAnimationFrame` deltas or the Long Animation Frames API; it's a config flip if you're already on Sentry or Datadog RUM, both of which expose frame timing percentiles.

Second, treat any frame over budget as a discrete event, not a rate. Log it with the call stack that caused it. The Long Animation Frames API (`LoAF`) in Chrome 123+ gives you exactly this — script attribution for any frame over 50ms, with timing for style, layout, paint, and the offending script. If your CI doesn't fail on a single 50ms+ LoAF in a critical user journey, you don't have a smoothness budget, you have a smoothness hope.

Third, on native or game stacks, audit your frame pacing for double-buffer vs triple-buffer behavior. Triple buffering hides occasional misses by absorbing them in latency. That's a tradeoff — input latency goes up, perceived smoothness goes up. Make it deliberate. Tonsky's post implicitly assumes a double-buffered, vsync'd target, which is the right default for input-sensitive UI but not for cinematic-style scrolling.

Looking ahead

The interesting question is whether the 'every frame perfect' framing makes it into the next round of Web Vitals or the React/Vue/Svelte profilers as a first-class metric, the way INP did. The signal from this HN cycle — 691 points, near-universal agreement, multiple Chromium and engine-team engineers in the comments saying 'yes, and here's the API' — suggests the vocabulary is now in the water. The next twelve months will tell whether the tooling follows or whether we spend another decade reporting averages and being surprised when users say it feels slow.

Hacker News 839 pts 274 comments

Every Frame Perfect

→ read on Hacker News

// share this

// get daily digest

Top 10 dev stories every morning at 8am UTC. AI-curated. Retro terminal HTML email.