Flutter is fast by default, but a few common habits can make an app stutter, especially on mid-range phones. Here are five mistakes that cause most performance problems, with a fix for each. Always measure before and after, using the steps at the end.
1. Rebuilding too much of the widget tree
Calling setState on a large parent widget rebuilds everything beneath it, even parts that did not change.
Problem:
class ProductPage extends StatefulWidget {
@override
State<ProductPage> createState() => _ProductPageState();
}
class _ProductPageState extends State<ProductPage> {
int quantity = 1;
@override
Widget build(BuildContext context) {
return Column(
children: [
HeavyProductGallery(), // rebuilds on every tap
Text('Qty: $quantity'),
ElevatedButton(
onPressed: () => setState(() => quantity++),
child: const Text('Add'),
),
],
);
}
}
Fix: move the changing part into its own small widget, so only that part rebuilds.
class QuantitySelector extends StatefulWidget {
const QuantitySelector({super.key});
@override
State<QuantitySelector> createState() => _QuantitySelectorState();
}
class _QuantitySelectorState extends State<QuantitySelector> {
int quantity = 1;
@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Qty: $quantity'),
ElevatedButton(
onPressed: () => setState(() => quantity++),
child: const Text('Add'),
),
],
);
}
}
The same idea applies with state management tools. Use selectors or scoped consumers so widgets listen only to the data they need.
2. Forgetting const constructors
A const widget is created once at compile time and Flutter can skip rebuilding it.
// Better
const Padding(
padding: EdgeInsets.all(16),
child: Text('Welcome'),
)
Turn on the prefer_const_constructors lint rule so the analyzer reminds you.
3. Building long lists the wrong way
Putting hundreds of items in a Column inside a SingleChildScrollView builds every item at once.
Fix: use ListView.builder, which builds only the items that are on screen.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index].name));
},
)
Avoid shrinkWrap: true on large lists, because it forces Flutter to measure all the children.
4. Doing heavy work on the UI thread
Parsing a large JSON file, processing images, or running complex calculations inside the main isolate blocks frames and freezes the UI.
Fix: move the work to another isolate.
final result = await Isolate.run(() => parseLargeJson(rawText));
On older Flutter versions, compute() does the same job. Network and disk calls that are already asynchronous do not block the UI, but the CPU-heavy work after them can.
5. Loading images larger than needed
Decoding a 4000-pixel photo to show it in a 100-pixel thumbnail wastes memory and time.
Image.network(
url,
cacheWidth: 300, // decode at roughly the displayed size
fit: BoxFit.cover,
)
For network images, use a caching package, and serve appropriately sized images from your backend. Also avoid expensive effects like many nested Opacity, ClipRRect, or BackdropFilter widgets in scrolling lists, because they can trigger costly offscreen rendering.
How to measure properly
- Run in profile mode on a real device:
flutter run --profile. Debug mode is much slower and misleading. - Open Flutter DevTools and use the Performance view to find frames that take longer than about 16 ms (for a 60 Hz screen).
- Use the widget rebuild tracker to see which widgets rebuild too often.
- Test on a low-end phone. If it feels smooth there, it will feel smooth everywhere.
Quick checklist
- Keep
setStateas local as possible - Add
constwherever you can - Use
ListView.builderfor long lists - Move heavy computation to an isolate
- Size images to how they are displayed
- Profile in profile mode, not debug mode
Fixing these five issues solves most performance complaints in real apps. [Add a real before/after DevTools screenshot from one of your own apps here.]
