How We Took a Flutter Auction Clock from Stuttering to 60fps

Published on March 16, 2025

5 min read
Jonas Ockerman
Jonas OckermanCo-Founder

When hundreds of buyers compete in a live fruit and vegetable auction, the clock on their screen is the only thing standing between a good deal and a missed one. The price drops every 50 milliseconds. Hesitate, and someone else bids first. See a stale price, and you bid on something you didn’t intend to.

We built the real-time auction clock for a Belgian platform that serves three major auction houses. The first version shipped fast and worked correctly: a custom-painted circular dial with 100 price dots, live WebSocket updates, and accurate bid submission. It did what it needed to do.

But “works correctly” and “feels right” are two different things. As the platform scaled and we observed real auction sessions, we saw room to push the experience further. The clock updated 20 times per second, which is technically fast enough. It just didn’t feel smooth. And on slower devices, we spotted a subtle gap between what buyers saw and what the system computed at bid time. Both were opportunities to raise the bar.

Where We Saw Room to Improve

The dot moved in steps, not in motion. The active dot had 100 discrete positions and jumped between them 20 times per second. Functionally correct, but visually choppy. On larger screens, where each jump covers more pixels, the effect was more noticeable. Buyers described it as “jittery.”

Slow devices introduced a timing gap. On lower-end tablets running at 5-10fps, the price shown on screen could trail reality by 100-200 milliseconds. At auction speed, that’s four price steps. If a buyer tapped “bid,” a naive implementation would recompute the price from wall-clock time and send a different value than the one on screen. We caught this early, but it needed a robust, permanent solution.

The Insight

Both opportunities pointed to the same architectural lever: the clock treated rendering as a single operation at a single frequency.

Every 50 milliseconds, the system recalculated the price, recolored all 100 dots, repositioned 10 labels, and moved the active dot in one paint call. Between those updates, nothing moved. The rendering model was discrete when part of it needed to be continuous.

Recognizing this distinction is what made the optimization straightforward. Not everything on the clock changes at the same rate or costs the same to draw. Once we separated those concerns, the solutions followed naturally.

What We Built

Split the Rendering Pipeline

We separated the clock into two painters with independent update frequencies. The expensive work (100 dots, 10 labels, conditional coloring) stays at the clock’s natural tick rate. The cheap work (one moving dot) runs at screen refresh rate. Each painter repaints independently. The dot never waits for the dial.

Fractional Interpolation

Instead of snapping to 100 integer positions, the active dot now interpolates between them using sub-step precision on every frame. The dot position is computed as a continuous value on every screen refresh, not just at tick boundaries. This gives the dot 3x more distinct positions per half-second, turning visible jumps into fluid motion.

Bid-Freeze Defense

When a buyer taps bid, we freeze the clock synchronously and capture exactly the price they saw, not a recomputed value from wall-clock time. Flutter processes gesture events before animation callbacks in its frame pipeline. We use that guarantee to stop the clock the instant the bid button is tapped, before the next frame can advance the price. The bid always matches what was on screen.

Frame-Skip Intelligence

A gating mechanism skips animation frames where nothing could have changed, cutting wasted computation by 50% without missing a single price step. At 60fps, the ticker fires every 16ms. But the price only changes every 50ms. The gate skips frames that fall between tick boundaries, reducing unnecessary computation across all visible clocks while keeping the margin tight enough to never miss a step.

The Results

The active dot now moves at 60 frames per second. Smooth, continuous motion regardless of how fast the clock ticks. The background dial still repaints at its natural rate, keeping CPU usage in check. The bid-freeze mechanism guarantees zero price drift on any device, verified through automated tests that simulate slow-frame scenarios.

We validated the full system under load: 500 concurrent users, real WebSocket connections, real-time audio streaming, bids coming in simultaneously. The clocks held steady.

What This Reinforced

Three principles stood out from this work that apply to any real-time Flutter application:

-Separate update frequencies by visual cost. If different parts of your UI change at different rates and have different rendering costs, they should repaint independently.
-Capture what the user saw, not what’s true now. On slow devices, the gap between displayed state and current state is real. For anything transactional, use the displayed value.
-Understand Flutter’s frame pipeline. Knowing that gestures run before animations, and that paint runs after layout, turns complex race conditions into straightforward solutions.

These aren’t tricks. They’re the kind of decisions that come from deep experience with Flutter’s rendering engine and from working on systems where correctness and smoothness both matter.


Building a real-time Flutter application and looking to push performance further?