Performance Tips
Flutter is fast by default, but real apps can still develop genuine performance problems as they grow — janky scrolling, dropped frames, slow list rendering. Most of these come down to a small set of recurring causes, and this lesson covers the practical habits that prevent them, plus the one rule that matters more than any individual tip: measure before you optimize.
Use const Constructors Wherever Possible
You've already seen const used throughout the widgets built in
UI Designs: Login Screen. It matters for more than
style — a widget constructed with const is built exactly once at compile time.
Flutter can recognize that a const widget instance is identical across rebuilds and
skip rebuilding it entirely, since a truly constant widget cannot have changed. Marking every widget
that genuinely never changes as const is one of the cheapest, highest-value performance
habits in Flutter.
Example: const Skips Unnecessary Rebuilds
// Rebuilt from scratch every time the parent rebuilds
Text('Static label')
// Built once — Flutter reuses this exact instance on later rebuilds
const Text('Static label')
Keep setState() Scoped to the Smallest Widget That Needs It
As covered in State Management: setState,
calling setState() rebuilds the entire widget it's called within, plus everything
below it in the tree. Calling it high up in a large widget — "lifting state up" further than
necessary — forces large, unrelated parts of the UI to rebuild for a change that only affects one
small piece. Extracting the part that actually changes into its own small
StatefulWidget, and calling setState() there instead, keeps rebuilds
limited to just what changed.
Use ListView.builder for Long Lists
As explained in Lists & ListView, a plain
ListView with an explicit children list builds every single item
immediately, even ones far off-screen. ListView.builder instead lazily builds only the
items currently visible (plus a small buffer), which is essential for any list that could contain
more than a small, fixed number of items — social feeds, search results, chat histories.
Example: Lazy Building for a Potentially Long List
ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
return ListTile(title: Text(messages[index]));
},
)
Isolate Expensive Widgets With RepaintBoundary
When one widget repaints frequently (an animation, a custom-painted chart), its neighboring
siblings can sometimes end up repainting unnecessarily too, simply because they share the same
paint layer. Wrapping the expensive widget in a RepaintBoundary gives it its own
separate paint layer, so its repaints don't force siblings to repaint along with it. Combined with
liberal use of const on the widgets around it, this keeps expensive animations or
custom painting from dragging down the rest of the screen.
RepaintBoundary(
child: CustomPaint(painter: ExpensiveChartPainter(data)),
)
Profile Before You Optimize
It's tempting to guess at what's "probably slow" and start rewriting it — but that habit wastes time and sometimes makes things worse. The DevTools Performance view, covered in Debugging & DevTools, records a real timeline of your app's frames and shows exactly which widgets and operations are actually consuming frame time. Measure first, form a hypothesis from real data, then optimize the specific thing the data points to — not whatever felt slow anecdotally.
Image Size and Caching
As touched on in Images & Assets, loading a huge source
image and simply scaling it down in the layout still costs the memory and decode time of the full
original size. Ship images sized appropriately close to their actual display size, and for images
loaded from the network, a package like cached_network_image avoids re-downloading
and re-decoding the same image on every rebuild or screen revisit.
Test On a Genuinely Low-End Device
An app that feels perfectly smooth on your development machine or the newest flagship phone can still stutter badly on the several-year-old, budget Android devices a real chunk of your users actually own. Periodically testing on an older or low-end device — or at minimum, a throttled emulator profile — surfaces real-world performance problems that a fast development machine will never reveal.