Why Your Flutter App Feels Slow on Real Phones and How to Fix It

A practical guide to Flutter performance: measuring in profile mode on a low-end Android phone, reading the DevTools frame chart, Impeller, cutting rebuilds, long lists, image memory, moving work to isolates, startup time, and app size.

Published 10 min read
Several Android and iOS phones lined up on a desk for app testing

The complaint usually goes like this. The app is smooth on the developer's iPhone, but on the budget Android phone that half of the actual users carry, scrolling stutters and the first screen takes four seconds to appear. Flutter is fast enough for almost any business app. When it feels slow, the cause is nearly always in a handful of places, and you can find them in an afternoon if you measure properly. Here is the process we follow.

Measure in Profile Mode on a Cheap Phone

Never judge performance in debug mode. Debug builds run Dart with a JIT compiler and extra assertions, and they can be several times slower than release. Run flutter run --profile instead, which compiles ahead of time like release but keeps the hooks DevTools needs. Do it on a real device, and pick a slow one. An entry-level Android phone with 3 or 4 GB of RAM tells you far more than a flagship or an emulator. Emulators run on your laptop CPU and can hide problems or invent new ones. Keep one cheap phone in the office for exactly this purpose.

Read the Frame Chart in DevTools

At 60 Hz each frame has about 16 ms. On a 120 Hz screen that budget drops to about 8 ms. Open the Performance view in Flutter DevTools and look at the frame chart. Each frame shows two bars. The UI thread bar covers your Dart code: build, layout, and anything else running in the main isolate. The raster thread bar covers turning the layer tree into GPU commands. If the UI bar is over budget, you are building too much or doing heavy work in Dart. If the raster bar is over budget, something is expensive to draw, typically Opacity or ShaderMask causing saveLayer calls, many clips, large blurred shadows, or huge images. Turn on Track Widget Builds to see which widgets rebuild in a janky frame, and use the rebuild counts in the Inspector to spot widgets rebuilding hundreds of times.

Impeller Fixed the Old Shader Jank

For years the worst Flutter jank came from shaders compiling the first time an animation ran. Impeller, the newer rendering engine, compiles its shaders at build time, so that first-run stutter is largely gone. Impeller has been the default on iOS since Flutter 3.10 and on most modern Android devices since Flutter 3.27. The old SkSL warm-up trick is no longer needed. Older or unusual Android GPUs can still behave differently, so include at least one older phone in testing. If you see a rendering glitch, compare with Impeller turned off to report the bug, but do not ship that as a fix.

Stop Rebuilding What Did Not Change

  • Calling setState at the top of a big screen rebuilds the whole screen. Move state down into the small widget that actually changes.
  • Split large build methods into separate widget classes, not helper methods. Flutter can skip a widget whose configuration did not change, but it cannot skip a method call.
  • Add const to constructors wherever the analyzer allows it. A const widget is the same instance every time, so Flutter skips rebuilding it.
  • Pass static subtrees through the child parameter of AnimatedBuilder and similar builders so they are built once, not every animation tick.
  • Never create a Future inside build, as in FutureBuilder(future: fetchOrders()). Every rebuild starts a new request. Create it once in initState or in your state layer.
  • Listen to the smallest slice of state you need. Our Flutter state management article covers how to do that with Riverpod and Bloc.

Long Lists

ListView with a children list builds every item up front. With 500 order rows, that is 500 widgets built before the first frame. ListView.builder builds only what is on screen plus a small buffer. If all rows have the same height, set itemExtent or prototypeItem so Flutter does not have to measure each row while scrolling. Avoid a ListView with shrinkWrap: true inside a Column or another scroll view. It forces every item to be laid out and quietly undoes the lazy loading. Use a CustomScrollView with slivers instead, and load data in pages of 20 to 50 items from the API.

Images Eat Memory

A 4000 by 3000 photo from a phone camera decodes to about 48 MB of memory, even if you show it as a 100 pixel thumbnail. A grid of those will crash a cheap phone. Set cacheWidth or cacheHeight on Image so it decodes at display size. Remember these are physical pixels, so multiply the logical size by the device pixel ratio. With cached_network_image, use memCacheWidth for the same effect. Better still, ask the backend for resized thumbnails. During development, set debugInvertOversizedImages to true and Flutter will show oversized images in inverted colours so they are easy to spot.

Move Heavy Work Off the UI Isolate

Your Dart code runs on one thread per isolate. Parsing a 2 MB JSON response can take well over 100 ms on a low-end phone, and during that time no frames are drawn. Isolate.run, available since Dart 2.19, runs a function in a short-lived background isolate and returns the result. The older compute function does the same thing. Spawning an isolate and copying data in and out costs a few milliseconds, so use it for work that takes longer than a frame, such as large JSON parsing, image processing, encryption, or building a PDF. On Flutter web, isolates are not available and compute runs on the same thread.

Startup Time and App Size

Slow startup is usually self-inflicted. A typical main function awaits Firebase, remote config, analytics, a local database, and a token refresh before calling runApp. Initialise only what the first screen needs, and start the rest after the first frame. Measure with flutter run --profile --trace-startup, which writes the time to first frame into a start_up_info.json file. For size, publish an Android App Bundle so Google Play delivers only the code for each device's CPU architecture, or use --split-per-abi if you distribute APKs directly. Run flutter build appbundle --analyze-size to see what takes space. Unused fonts, uncompressed PNG assets, and large packages are the usual suspects. Building with --obfuscate and --split-debug-info trims a little more and keeps symbols for crash reports.

Symptom, Cause, Fix

SymptomLikely causeFix
Scrolling a long list stuttersListView with children, or shrinkWrap inside another scroll viewListView.builder or slivers, with itemExtent when rows have fixed height
Typing in a text field lagssetState high in the tree rebuilds the whole formMove state into the field widget, use const and smaller widgets
App crashes or reloads on image-heavy screensFull-size photos decoded for small thumbnailscacheWidth or memCacheWidth, server-side thumbnails
UI freezes for a moment after an API callLarge JSON parsed on the UI isolateIsolate.run or compute, smaller API responses
Raster bar is red during animationsOpacity, clips, blurs, or big shadows causing saveLayerFadeTransition or AnimatedOpacity, simpler effects, RepaintBoundary around animated parts
Blank screen for several seconds at launchToo many awaits in main before runAppInitialise lazily after the first frame

A Checklist Before Each Release

  1. 1Build in profile mode and test the five most used screens on the slowest phone you support.
  2. 2Scroll every long list quickly and watch the frame chart for red frames.
  3. 3Run with debugInvertOversizedImages once and fix any inverted image.
  4. 4Check startup time with --trace-startup and compare it with the previous release.
  5. 5Run --analyze-size and look for anything that grew by more than a megabyte.
  6. 6Look at crash and ANR reports in Firebase Crashlytics or the Play Console for out-of-memory errors.

Do not optimise what you have not measured. Most of the time the slow part is not where the team thought it was.

Most of these fixes are small once someone knows where to look. Our Flutter training for teams includes a hands-on session where participants profile a deliberately slow app with DevTools and fix it step by step, which tends to change how people write widgets from then on.

Key takeaways

  • Measure in profile mode on a real low-end Android phone, never in debug mode or only on an emulator.
  • Use the DevTools frame chart to tell UI thread problems from raster thread problems before changing code.
  • Reduce rebuilds with smaller widget classes, const constructors, and state placed close to where it is used.
  • Use ListView.builder for long lists and decode images at display size with cacheWidth.
  • Move parsing and other heavy work to Isolate.run, and keep main lean so the first frame appears quickly.
Share this article

Related articles

More articles on software development, AI, cloud, and infrastructure.

Smartphone with app wireframes next to a notebook and calculator on a deskSoftware Development

10 min read

How Much Does It Cost to Build a Mobile App in Indonesia?

Indicative price ranges in Rupiah for simple, medium, and complex mobile apps, what really drives the number, Flutter versus two native apps, team and timeline, the yearly costs after launch, and how to compare vendor quotes.

Let’s talk

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation