The Flutter Habits That Quietly Decide Whether An App Feels Fast

Two Flutter apps can use the same framework, the same version of Dart, and the same target devices, yet one opens instantly and scrolls without a hitch while the other stutters the moment a list gets long. The framework rarely gets the blame it deserves for this gap, because the difference almost never comes from Flutter itself. It comes from a handful of habits in how the widget tree, state and rendering pipeline get used from the first commit onward.

Key Takeaways

  • Rebuilding the whole widget tree on every state change is the single biggest cause of Flutter jank, and it is usually fixed by scoping state updates to the smallest widget that actually needs to change.
  • The `const` keyword is not cosmetic. Marking widgets `const` wherever their properties never change lets Flutter skip rebuilding them entirely, which compounds across a deep widget tree.
  • Long or nested lists need lazy builders such as `ListView.builder` rather than eagerly constructed children, because building every row up front defeats the point of virtualised scrolling.
  • Heavy image assets, not complex layouts, are the most common cause of dropped frames on real devices, and resizing images before they reach the widget tree solves most of it.
  • Performance problems that show up on a fast development phone often go unnoticed until testing happens on the mid-range hardware a large share of real users actually own.

None of this is exotic. Flutter’s own rendering pipeline is fast; the framework consistently benchmarks well against native toolkits for raw frame throughput, and that got measurably better once Impeller became the default rendering engine for iOS and Android on API 29 and above as of Flutter 3.27 in December 2024, replacing the older Skia backend and removing a class of shader-compilation jank that used to show up as a stutter on a widget’s first paint. The apps that still feel slow are almost always the ones where a rebuild storm, an unbounded list or an oversized image asset quietly overwhelms a pipeline that was never the bottleneck in the first place. That risk is only growing: GitHub’s Octoverse 2025 report found that AI-assisted repositories nearly doubled to 4.3 million over the year, and code written faster than it is reviewed is exactly the kind of code where an unnecessary rebuild or an unresized image slips through unnoticed.

Stop rebuilding more than you need to

Flutter’s declarative model means the framework rebuilds a widget by calling its `build` method again, and by default a `setState` call on a parent widget rebuilds every descendant beneath it. On a small screen this is invisible. On a screen with a scrolling feed, a live counter or an animated header, calling `setState` at too high a level in the tree can force hundreds of widgets to rebuild sixty times a second even though only one small label actually changed.

The fix is almost always to push state down. A counter widget, a like button or a form field should hold its own state where possible, rather than living inside a parent that manages everything through one shared object. Where shared state genuinely has to live higher up, a state management approach that supports granular listening, such as `ValueNotifier`, `Provider` with selective `Selector` widgets, or Riverpod’s fine-grained providers, lets only the widgets that depend on a particular value rebuild when it changes. The official Flutter performance documentation is direct about this: the goal is never zero rebuilds, it is making sure each rebuild is cheap and scoped to what actually changed.

Close-up of a hand typing on a laptop keyboard with code visible on the screen

Treat const as a performance feature, not a style choice

Many Flutter teams treat the `const` keyword as a linter preference, something added during a tidy-up pass rather than a deliberate choice. That undersells what it actually does. A widget constructed with `const` is built once and reused by reference on every subsequent build, which means Flutter can skip the rebuild for that subtree entirely rather than diffing it and finding nothing changed.

Marking a widget const does not make it faster to build once. It makes every future rebuild of that subtree free, which matters far more as an app’s screen count grows.

The saving on any single widget is small, often microseconds, but it compounds across a real app’s widget tree, which can run to hundreds of nested widgets on a single busy screen. Static icons, fixed padding, unchanging text labels and decorative containers are all obvious candidates, and the Dart analyzer will flag most missed opportunities automatically if the `prefer_const_constructors` lint is enabled from the start of a project rather than retrofitted later.

Build lists lazily, not eagerly

A `Column` filled with a hundred items and a `ListView.builder` covering the same hundred items look identical on screen and behave completely differently underneath. The `Column` constructs every child widget the moment the screen builds, whether or not it is ever visible. `ListView.builder` constructs only the items currently on screen, plus a small buffer, and builds more as the user scrolls.

This distinction matters more as content grows. A list of ten items rarely shows a difference either way. A feed, a chat history or a product catalogue that can grow into the hundreds will visibly stutter on first load if it is built eagerly, because the app is doing far more layout work than the screen can display at once. The same logic extends to grids, where `GridView.builder` plays the equivalent role, and to any widget that renders a variable-length collection pulled from an API or a database.

Overhead view of two hands typing on a laptop showing a code editor in dim light

Respect the weight of images

Image decoding is one of the most expensive operations Flutter performs on the main thread, and it is also one of the easiest to get wrong by accident. A photo captured at four thousand pixels wide, displayed inside a two-hundred-pixel thumbnail, still gets decoded at something close to its original resolution unless the app explicitly tells Flutter otherwise, because the framework has no way to know the final display size in advance without a hint.

Setting `cacheWidth` or `cacheHeight` on an `Image` widget, or resizing assets server-side before they are ever downloaded, avoids decoding far more pixels than the screen can show. Google’s own guidance on rendering performance makes the same point in a browser context: shipping an asset larger than its display size is pure wasted work, and the cost scales with how many of those oversized images appear on a single screen at once. For a mobile app, where memory pressure is tighter and the penalty for a dropped frame is more visible, the effect is proportionally worse.

A hand holding a smartphone showing its home screen app icons

Test on the device your users actually own

A Flutter app that runs smoothly on a current-generation development phone can behave very differently on the mid-range hardware that a large share of any app’s real user base is carrying. Frame budgets are tightest exactly where the CPU and GPU are slowest, so a rebuild pattern or an oversized asset that is invisible on a flagship device can be the difference between a smooth scroll and a visibly stuttering one on a three-year-old mid-tier phone.

Nielsen Norman Group’s research on response time thresholds, first published in 1993 and still the standard reference on the subject after a 2014 update, is a useful reminder of why this matters beyond raw frame rates: users perceive a delay of around a second as an interruption to their flow, well before it reaches the point of feeling like the app has frozen. A development team building and testing exclusively on their own newest phones will consistently underestimate how close to that threshold their real users are sitting. Teams building Flutter apps professionally, including the mobile work handled by app development studios such as Arch, tend to build device testing across a deliberately mixed hardware tier into the release process rather than leaving it to chance late in a project.

Several blank smartphone mockups arranged together to represent testing across multiple devices

Frequently Asked Questions

Why does my Flutter app feel slow even though the framework is supposed to be fast?

Flutter itself is rarely the bottleneck. Slowness almost always comes from avoidable patterns in the app’s own code, most commonly rebuilding too much of the widget tree on every state change, decoding oversized images, or building long lists eagerly instead of lazily.

Does using the const keyword actually make a measurable difference?

Yes, particularly as a screen’s widget tree grows. A const widget is built once and reused on every subsequent rebuild rather than reconstructed, and that saving compounds across the hundreds of widgets a typical busy screen contains.

What is the difference between ListView and ListView.builder for performance?

A plain list widget constructs every item immediately regardless of whether it is visible, while ListView.builder constructs only the items currently on or near screen. The difference is negligible for short lists and significant for anything that can grow into the hundreds.

How much does image size actually affect Flutter app performance?

Substantially. Decoding an image at a resolution far larger than its display size wastes CPU and memory on every frame it appears, and this is one of the most common causes of dropped frames on real devices, particularly on mid-range hardware.

Should Flutter apps be tested on older or cheaper devices before release?

Yes. Performance issues that are invisible on a current flagship phone are frequently obvious on the mid-range hardware that makes up a large share of any real user base, so testing exclusively on newer devices tends to hide problems until after release.

Sources

Leave a comment