# Improve Your App's Performance: 5 Effective Tips

Improving performance isn't about fancy tech — it's about a handful of high-leverage habits, applied consistently. Here are the five that matter most, each with enough code to actually show the mechanism rather than just name it. If any one of these grabs you, I've gone deeper on rebuilds, DevTools, and isolates in separate posts linked at the end.

## 1\. Use const constructors — and see what they actually skip

`const` doesn't just save a little compile-time work — it tells Flutter the widget and everything inside it are identical to last time, so the whole subtree gets skipped during a rebuild rather than rebuilt and found unchanged. The difference is easiest to see with a debug print on both a `const` and a non-`const` sibling:

```dart
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () => setState(() => count++),
            child: Text('Count: $count'), // not const — reads live state, must rebuild
          ),
          const ExpensiveHeader(), // const — Flutter skips rebuilding this entirely
        ],
      ),
    );
  }
}

class ExpensiveHeader extends StatelessWidget {
  const ExpensiveHeader({super.key});
  @override
  Widget build(BuildContext context) {
    debugPrint('ExpensiveHeader build'); // never prints again after the first frame
    return const Text('Welcome back');
  }
}
```

Tap the button repeatedly and `ExpensiveHeader build` prints exactly once — not once per tap. The `Text('Count: $count')` line necessarily rebuilds because it reads `count`; `ExpensiveHeader` doesn't, and marking it `const` is what lets Flutter prove that to itself and skip the work rather than redo it and get the same answer.

## 2\. Scope rebuilds with Selector, not just "use a state management library"

Reaching for Provider or Riverpod doesn't automatically fix rebuild scope — a widget that calls `context.watch<CartModel>()` rebuilds on *any* change to `CartModel`, even a field it doesn't render. `Selector` (Provider) and `.select()` (Riverpod) narrow that down to the one field actually being displayed:

```dart
// Rebuilds on ANY change to CartModel, even unrelated ones
Widget buildBadge(BuildContext context) {
  final cart = context.watch<CartModel>();
  return Text('${cart.itemCount}');
}

// Rebuilds ONLY when itemCount specifically changes
Widget buildBadge(BuildContext context) {
  return Selector<CartModel, int>(
    selector: (context, cart) => cart.itemCount,
    builder: (context, itemCount, child) => Text('$itemCount'),
  );
}
```

If `CartModel` also tracks a shipping address or a discount code, the first version rebuilds this badge every time either of those changes for no visible reason; the second doesn't. The rule worth remembering: the state management library decides *how* state is shared, but `Selector`/`.select()` is what actually decides *how much* rebuilds when it changes — and skipping that step is the most common way teams adopt Provider or Riverpod and see no rebuild improvement at all.

## 3\. Profile before you guess — with the actual commands

Performance hiccups are rarely where intuition points. Run in profile mode, not debug mode, since debug mode carries extra overhead that skews the numbers:

```bash
flutter run --profile
```

Open DevTools (via your IDE's "Open DevTools" button, or `dart pub global run devtools` from the CLI) and go to the **Performance** tab. Reproduce the slow interaction, then read the frame chart: a bar with a tall *UI-thread* segment points at your `build()` methods doing too much; a tall *raster-thread* segment points at images or shader work instead. That distinction changes which of tips 1, 2, or 4 actually applies — optimizing `build()` methods won't help a raster-bound problem, and vice versa. \[I've gone through a full worked example of this, including catching a real dropped frame, in a separate post on Flutter DevTools.\]

## 4\. Streamline assets with the settings that actually matter

`FadeInImage` makes an image transition look smoother, but it doesn't address the bigger cost: decoding a full-resolution image into memory just to display it at a fraction of that size. `cacheWidth`/`cacheHeight` fix the actual cost, not just the visual polish:

```dart
Image.asset(
  'assets/banner.png',
  cacheWidth: 400, // decode at display size, not the source file's full resolution
  cacheHeight: 200,
)
```

A 4000×2000 source image displayed in a 400×200 tile costs roughly 100x the memory to decode without this — `cacheWidth`/`cacheHeight` tell Flutter's image cache to decode at the size you'll actually render, which is almost always the single highest-leverage image fix available, ahead of compressing the source file itself.

For genuinely heavy work — image manipulation, large JSON parsing — move it off the UI thread entirely:

```dart
Future<Uint8List> resizeInBackground(Uint8List bytes) {
  return compute(_resize, bytes);
}

Uint8List _resize(Uint8List bytes) {
  // decode, resize, re-encode — all off the UI isolate
  return processedBytes;
}
```

\[`compute()` and its newer counterpart `Isolate.run()` are covered in more depth, including when the overhead of an isolate is and isn't worth it, in a separate post on Dart isolates.\]

## 5\. Simplify the widget tree — and see the difference in the Inspector

A deeply nested tree costs real layout time, since Flutter has to walk every level to compute constraints. The fix is usually flattening redundant wrapper widgets, not adding new abstraction:

```dart
// Before: three nested widgets doing the job of one
Container(
  padding: const EdgeInsets.all(8),
  child: Center(
    child: Container(
      child: const Text('Hello'),
    ),
  ),
)

// After: same visual result, one widget
const Padding(
  padding: EdgeInsets.all(8),
  child: Center(child: Text('Hello')),
)
```

Open DevTools' **Flutter Inspector**, enable **Show guidelines**, and compare the widget tree depth before and after a change like this on a real screen — a tree that's meaningfully shallower after removing redundant `Container`s and `SizedBox`es is a concrete, visible confirmation that the simplification actually reduced structure, not just line count.

## Wrapping up

Five habits, roughly in order of how often they matter: mark static subtrees `const`, scope rebuilds to the specific field a widget renders rather than the whole model, profile instead of guessing which of the two applies, decode images at display size rather than source size, and flatten a widget tree that's grown more nested than the UI it produces requires. Each one is small on its own; applied together across a real app, they compound.

For more depth than a five-tip roundup can cover: the mechanics of *why* rebuilds cascade the way they do are in [The Hidden Cost of Rebuilds in Flutter](https://gidudunicholas.dev/the-hidden-cost-of-rebuilds-in-flutter), a full DevTools walkthrough with a worked jank-hunting example is in [A Friendlier Guide to Taming Flutter Jank with DevTools](https://gidudunicholas.dev/a-friendlier-guide-to-taming-flutter-jank-with-devtools), and when an isolate is and isn't worth its overhead is covered in the isolates post on this blog.

* * *

**References**

1.  [Flutter performance best practices](https://docs.flutter.dev/perf/best-practices)
    
2.  [Selector — provider package documentation](https://pub.dev/documentation/provider/latest/provider/Selector-class.html)
    
3.  [Image.cacheWidth and cacheHeight](https://api.flutter.dev/flutter/widgets/Image-class.html)
    
4.  [Flutter DevTools: Performance page](https://docs.flutter.dev/tools/devtools/performance)
    
5.  [Flutter DevTools: Widget Inspector](https://docs.flutter.dev/tools/devtools/inspector)
