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.

Honest rule: a change made without profiling first is a guess, not an optimization — and guesses sometimes make performance worse while adding code complexity for no real benefit.

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.

Next up: head to Best Practices, the final capstone lesson pulling together the habits from this entire track.