A Flutter app that drops frames feels cheap no matter how nice it looks. The framework is fast by default, but it is easy to undo that with a few careless habits. I have spent enough time in the profiler to know where the time goes, and most of the wins come from a short list of fixes rather than clever tricks. Here is what I check first.
Measure before you change anything
Never optimise on a hunch. Run the app in profile mode, not debug mode, because debug builds are deliberately slow and will lie to you. The DevTools performance view shows you the frame timeline, and anything that pushes a frame past sixteen milliseconds is your problem. Find the actual slow frame before touching code, or you will spend an afternoon speeding up something nobody noticed.
flutter run --profile
# then open DevTools and record the performance timeline
Use const wherever you can
A const widget is built once and reused, so the framework skips rebuilding it. This is the cheapest performance win in Flutter and most apps leave it on the table. If a widget and its inputs never change, mark it const. The analyzer can even flag the spots for you if you turn on the right lint.
// rebuilt every time the parent rebuilds
Padding(padding: EdgeInsets.all(8), child: Text('Hi'))
// built once and cached
const Padding(padding: EdgeInsets.all(8), child: Text('Hi'))
This matters most inside widgets that rebuild often, which loops back to choosing the right approach in Flutter state management. A tight rebuild scope plus const widgets keeps the work the framework does each frame small.
Build long lists lazily
The single most common mistake I see is building a long list with a Column inside a scroll view. That constructs every item up front, even the thousands off screen. ListView.builder only builds what is visible plus a small buffer, so memory and build time stay flat no matter how long the list is. For any list that can grow, use the builder.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
)
Keep work off the main thread
The UI runs on one thread, and anything heavy you do there steals time from rendering. Parsing a large JSON payload, resizing an image, or running a slow computation will freeze the interface. Move that work to a background isolate with the compute helper so the UI thread stays free to draw frames.
import 'package:flutter/foundation.dart';
Future<List<Item>> parseItems(String json) {
return compute(decodeItems, json);
}
- Profile in profile mode, never debug mode
- Mark unchanging widgets const
- Use builder constructors for long or growing lists
- Push parsing and heavy math to an isolate
- Cache and size images instead of loading full resolution
Images are usually the hidden cost
Images eat memory faster than anything else. A photo loaded at full resolution into a small thumbnail wastes most of that memory. Set a cache width that matches the display size so the framework decodes a smaller bitmap. For network images, use a caching package so you fetch each one once rather than on every scroll.
Watch your shaders on first run
The first time an animation runs, Flutter may compile shaders, which causes a one off stutter that users notice on a fresh install. You can warm up shaders during a splash screen so the jank happens before the user is watching. This is a small detail, but first impressions stick. Native code paths can also affect startup, which is why I keep platform work lean as described in Flutter platform channels. Performance is rarely one big fix. It is a dozen small ones, each measured, each kept.