skip to content

How would you set and enforce a frame-time budget for a Flutter team's app that ships to both 60 Hz and 120 Hz devices?

level: principalimportance: nice to knowfreq 22%

answer

  1. budget per refresh rate, per thread
  2. headroom below 1000/X ms
  3. reference devices in profile mode
  4. field data from FrameTiming
  5. gate regressions, not single frames

basics

~20 s

Express the budget per thread as 1000 divided by the refresh rate with headroom, so about 16 ms or 8 ms. Check it in profile mode on reference devices, watch the field with FrameTiming percentiles, and gate on regressions, not single frames.

solid answer

~50 s

A useful budget is **per thread and per refresh rate**: UI and raster time each under `1000 / X` ms, about 16.7 ms at 60 Hz and 8.3 ms at 120 Hz, with headroom because devices vary and background work steals time. Pick **reference devices**, including a slow one, and measure key journeys (cold open, main scroll, the heaviest screens) in **profile mode**. Record traces for each release so regressions have a baseline. In the field, collect `FrameTiming` through `SchedulerBinding.instance.addTimingsCallback`. Report the share of frames over budget per thread and duration percentiles per screen, segmented by refresh rate and device class. Enforce it by comparing each build's lab numbers against the baseline and alerting on field percentiles, not by failing on a single slow frame. No threshold is universally right; it trades the product's feel against engineering cost.

go deeper

for a junior

Know that the frame budget depends on the display's refresh rate: about 16 ms at 60 Hz and 8 ms at 120 Hz.

for a middle

Explain why the budget applies to UI and raster time separately, and which FrameTiming values measure each.

for a senior

Build the measurement: reference devices in profile mode, baseline traces, and field FrameTiming aggregated by segment.

for a principal

Own the trade-offs: which devices set the floor, whether 120 Hz is a promise, and when a performance regression blocks a release.

## Why a budget needs deciding "Keep it at 60 fps" was an adequate rule when every phone refreshed at 60 Hz. Many current devices run at 90 or 120 Hz, where Flutter tries to match the display. The budget per frame shrinks from about 16.7 ms to 8.3 ms, and a screen that was smooth on one phone stutters on another. A team needs an explicit, measurable definition of "fast enough". Choosing it is a judgement call. ## Defining the budget - **Per thread.** UI and raster run as a pipeline, so each must fit the frame on its own. A budget on their sum is too strict for missed frames and says nothing about which thread to fix. - **Per refresh rate.** Express it as `1000 / X` ms of the display rate, rather than a fixed 16 ms. This is the rule `FrameTiming`'s own docs use. - **With headroom.** Aim below the limit, because the same code runs slower on older devices and when the OS is busy. How much headroom is a product decision. - **Per journey.** A settings page and a photo editor do not deserve the same scrutiny. Name the journeys that matter, such as app open, the main feed scroll and the heaviest editor, and budget those. - **With latency in mind.** When UI plus raster exceeds one frame, frames still arrive on time but input lags. For drag-heavy or drawing screens, also track `totalSpan`. ## Measuring in the lab 1. Choose **reference devices** that include the slowest one you support and a high refresh rate phone. 2. Run journeys in **profile mode**. Debug numbers are not comparable, and emulators do not reflect real GPUs. 3. Keep **baseline traces** per release, so a new trace can be compared with the last good one. 4. Automate the recording where possible, so it does not depend on someone remembering. Recording timelines from automated tests has its own tooling. ## Measuring in the field `SchedulerBinding.instance.addTimingsCallback` works in release builds with negligible overhead, so the real distribution can be measured: | Metric | From | Why | |---|---|---| | share of frames with `buildDuration` over budget | UI thread | Dart-side regressions | | share of frames with `rasterDuration` over budget | raster thread | scene-cost regressions | | p90 / p99 of each duration per screen | both | tail behaviour users notice | | `totalSpan` over budget on interactive screens | both | input latency | Segment by **refresh rate**, **device class** and **app version**, or improvements on flagships will hide regressions on low-end phones. Aggregate on the device and send summaries. ## Enforcing it - **Gate on trends and percentiles**, not single frames. The first frame of a route and one-off shader work will always produce outliers. - **Compare against a baseline.** A 15 % increase in over-budget frames on a journey is a signal even if the absolute value still passes. - **Give each journey an owner**, so a regression has someone to act on it. - **Budget the fix, too.** Sometimes the answer is to accept a looser target on a rarely used screen and spend the effort where users spend their time. ## Judgement calls a lead owns - Whether 120 Hz smoothness is a promise or a best effort. - Which devices define the floor. Supporting very old phones may cost more than it earns. - How much engineering time performance work gets against features, and when a regression blocks a release. - Whether lab numbers or field numbers win when they disagree. Field data reflects real use; lab data is reproducible.

  • Why not simply require every frame to be under 16 ms?
    It is wrong in both directions. On a 120 Hz display 16 ms is two frames, so the rule allows visible jank. And every app has occasional outliers, such as a route's first frame or first-use shader work, so a zero-exceptions rule fails constantly and gets ignored. Percentiles per thread and per refresh rate, against a baseline, are enforceable.
  • How do you keep field frame data from being dominated by high-end devices?
    Segment it. Report over-budget shares and percentiles separately by device class and refresh rate, and set the budget against the reference slow device's segment. An aggregate average mixes fast phones that never miss with slow ones that often do, and hides the users who suffer.

saying these in an interview costs you the question

  • One global rule of 16 ms per frame covers every device
  • The budget applies to UI time plus raster time added together
  • Debug-mode measurements are good enough for a performance baseline
  • A single slow frame in CI should fail the build
  • Averages across all devices show whether low-end users see jank