Flutter Performance: how to find and fix jank, slow lists and memory problems
Why Flutter apps stutter and what to do about it — rebuild scope, list building, image sizing, expensive widgets, and how to measure with DevTools instead of guessing.
Flutter apps are fast by default. When one stutters, the cause is usually not the framework but something the app asks it to do on every frame. This guide covers how to measure the problem properly and the fixes that account for most real-world jank.
What “smooth” actually means
At 60Hz the device has about 16 milliseconds to build, lay out and paint a frame. On a 120Hz screen, about 8. Miss the budget and a frame is dropped, which the eye reads as a stutter. Two separate threads matter: the UI thread runs your Dart code, and the raster thread turns the result into pixels. Knowing which one is over budget changes the fix entirely.
Measure before changing anything
Guessing at performance wastes days. Three tools cover most cases.
Always profile in profile mode
Debug builds are far slower than release builds and will mislead you. Run flutter run --profile on a physical device. Never judge performance on an emulator or in debug mode.
DevTools Performance view
Record while reproducing the stutter, then look at the frame chart. Tall bars on the UI track mean your Dart work is too slow — usually building too many widgets or doing computation in build(). Tall bars on the raster track mean painting is too expensive — usually shadows, blurs, opacity or clipping.
Rebuild counters
Turn on “Track widget rebuilds” in DevTools. If a widget rebuilds when unrelated state changes, that is a scope problem, and it is the most common cause of sluggish screens.
Fix 1: shrink the rebuild scope
The most frequent mistake is rebuilding a large subtree to update a small piece of it. The fix is to move the listener as close to the value as possible.
// Rebuilds the whole page on every change
Consumer<CartModel>(
builder: (_, cart, __) => Scaffold( /* entire page */ ),
)
// Rebuilds only the badge
Scaffold(
appBar: AppBar(actions: [
Consumer<CartModel>(
builder: (_, cart, __) => Badge(count: cart.items.length),
),
]),
body: const ProductList(), // const: never rebuilt
)Note the const. A const widget is created once and skipped on subsequent rebuilds. Marking leaf widgets const wherever possible is free performance, and the analyzer can enforce it with the prefer_const_constructors lint.
Fix 2: build lists lazily
ListView(children: [...]) builds every child immediately, even the thousand below the fold. ListView.builder builds only what is visible.
- Use
ListView.builderorSliverListfor any list that is not trivially short. - Give items a stable
keyso Flutter can reuse elements when the list reorders. - Avoid
shrinkWrap: trueon long lists; it forces measuring every child. - Do not nest scrollables. Use
CustomScrollViewwith slivers instead. - Keep item widgets cheap. A list row that builds a complex layout, formats dates and parses strings will stutter no matter how lazily it is built.
Fix 3: size images to the screen, not the server
Images are the biggest memory consumer in most apps, and a common cause of raster-thread jank. A 4000px photo displayed in a 100px avatar still decodes at full size unless told otherwise.
Image.network(
url,
cacheWidth: 200, // decode at the size actually displayed
cacheHeight: 200,
)Better still, request an appropriately sized image from the backend or CDN. Use a caching image package so scrolling back up does not re-download, and always provide width and height so layout does not shift when the image arrives.
Fix 4: watch the expensive paint operations
These are cheap individually and costly in a scrolling list:
- Opacity.
Opacitytriggers a saveLayer. PreferAnimatedOpacityon small subtrees, or fade with a colour where possible. - Clipping.
ClipRRecton every card is measurable. ABoxDecorationwithborderRadiusis usually cheaper. - Shadows and blur.
BackdropFilteris the most expensive common widget in Flutter. One is fine; one per list item is not. - Large
Stacks with overlapping transparency.
Fix 5: keep work out of build()
build() can run many times per second. It should assemble widgets and nothing else. Move these out:
- JSON parsing, sorting and filtering of large lists
- Date and number formatting in a loop
- Creating controllers or streams — those belong in
initState - Regex compilation
For genuinely heavy computation, move it off the UI thread entirely with compute() or an isolate. Parsing a large API response is a good candidate.
Fix 6: animate without rebuilding
An AnimationController that calls setState on every tick rebuilds the subtree sixty times a second. AnimatedBuilder with a child passed in rebuilds only the animating wrapper and reuses the child.
AnimatedBuilder(
animation: controller,
child: const ExpensiveContent(), // built once
builder: (_, child) => Transform.rotate(
angle: controller.value * 6.28,
child: child,
),
)App size and startup
Perceived performance starts before the first frame.
- Build an app bundle so users download only what their device needs, and
--split-per-abiif distributing APKs directly. - Audit dependencies. Each package adds code, and some pull in large transitive trees.
- Compress and right-size bundled assets. Shipping 2MB PNGs that display at 200px is common and avoidable.
- Do not block the first screen on network calls. Render the layout, then fill it in.
A practical order of attack
- Reproduce the jank on a real device in profile mode.
- Check DevTools: is the UI thread or the raster thread over budget?
- UI thread → narrow rebuild scope, move work out of
build(), addconst. - Raster thread → look for opacity, clipping, blur and oversized images.
- Re-measure. Keep only the changes that show up in the numbers.
Most apps do not need exotic optimisation. Narrow rebuilds, lazy lists and correctly sized images account for the large majority of real performance problems.
Read next
Flutter State Management: setState, Provider, BLoC and Riverpod compared
A practical guide to choosing state management in Flutter. What each option is good at, where each breaks down…
Payment Gateways in Flutter: Razorpay, PayU, CCAvenue and Stripe compared
How mobile payment integration actually works, how the main gateways differ for Indian and global apps, and th…