Skip to content
Mobile9 min

The Flutter performance audit checklist we run before every release

Seven specific checks that catch most performance regressions before users do. Profiler signals, real numbers, no listicles.

The Flutter performance audit checklist we run before every release

"Flutter is fast" is true on the demo screen and false in production once you've added 12 packages, 4 image assets, and a list view that updates every 200ms. The framework is fast; what you build on top of it has to stay fast deliberately.

We run the same seven-point audit before every release. Each check has a profiler signal that tells you whether you're affected. None of them are surprising in isolation — the value is in running all seven, every time, before users hit them.

1. Frame budget — 16ms or 8ms

Open DevTools, switch to the Performance tab, record on the worst device you support. Anything red on the timeline is over budget — 16ms for 60fps targets, 8ms for 120fps. Common offenders we see: `Image.network` without `precacheImage`, complex `CustomPainter` without `shouldRepaint`, `Hero` widgets with expensive children.

Pin a device-specific budget per release. "It's smooth on my Pixel 8" isn't a budget; "frame budget on a Samsung A52 is 16ms with 95th percentile under 22ms" is.

2. Rebuild profiling and const constructors

Turn on the Rebuild Stats overlay in DevTools. Walk through your screens. Anything rebuilding when nothing visible changed is wasting CPU.

Common fix: const constructors keep widget identity, so the framework can skip the rebuild entirely. The pattern that matters is moving const widgets out of stateful build methods so they don't get re-instantiated. Riverpod's `select` (or Provider's `Selector`) gives the same benefit at the state-management layer.

3. Image asset sanity

Wrong-resolution image assets are the easiest 30%-improvement on a list-heavy screen. A 2000x2000 hero image displayed at 200x200 wastes GPU upload bandwidth on every scroll.

Three checks: use `cacheWidth` and `cacheHeight` on Image to downsample at decode time; pre-decode with `precacheImage` in initState for above-the-fold imagery; ship WebP for photos and PNG only when you need transparency.

4. List performance

`ListView` with 50+ children is wrong — it builds them all at once. Use `ListView.builder`. If items have a known height, set `itemExtent` or `prototypeItem` so the framework can skip layout for off-screen items.

Avoid `AutomaticKeepAlive` unless you genuinely need state preserved across scroll. Every kept-alive child is a memory cost that compounds.

5. Isolate boundaries for CPU work

JSON parsing of >100KB payloads, image processing, encryption, regex over large strings — all of it belongs on a worker isolate via `compute()` or a dedicated `Isolate.spawn`. The CPU profiler will show you main-isolate frame stalls if you're getting it wrong.

Async doesn't mean concurrent. `await jsonDecode(largePayload)` still blocks the main isolate during the decode itself. The await only yields between microtasks — it doesn't move the work off-thread.

6. Network and disk I/O hygiene

Cancel in-flight requests on widget dispose. Otherwise users navigating away from a screen still pay for the response parse and any setState that follows — including the warnings about setState-after-dispose that you're suppressing.

Move SQLite/Hive writes off the main isolate when they're more than trivially small. Pagination over full-fetch for any list bigger than 50 items.

7. Cold start, memory, and bundle size

Cold start: `flutter run --profile`, measure first-frame time on the worst device. If you've added a heavy package recently, this catches it.

Memory: 5 minutes of typical user flow, watch the Memory tab. Steady growth without GC reclaim means a leak — usually a stream subscription or controller you forgot to dispose.

Bundle size: `flutter build appbundle --analyze-size`. Anything new and large in the report needs justification. We've removed a 12MB package after seeing what it added to the bundle.

Run this audit before every release, log the actual numbers somewhere, and you'll catch the regressions before users do. The compounding effect across a year of releases is the difference between an app that feels fast and one that's "OK on flagship phones, sluggish on a Samsung A52".

None of these are clever. They're disciplined. That's the point.

Working on something like this?

We build production software for teams whose problems don't fit a template. Tell us what you're working on — we'll tell you how we'd build it.

Start a conversation