Flutter Performance Optimization: Fix Jank and Dropped Frames
Flutter can render extremely smooth interfaces, but poorly optimized widgets, expensive synchronous work, excessive rebuilds, and inefficient lists can cause jank and dropped frames.
In this tutorial, we will look at a systematic approach to finding and fixing Flutter performance problems.
What Is Jank?
A smooth UI needs to produce frames quickly enough to keep animations responsive. When a frame takes too long to render, the user notices stuttering or delayed interaction.
Common causes include:
- Expensive widget builds
- Large synchronous computations
- Excessive widget rebuilds
- Poorly configured lists
- Large images
- Expensive layout or painting
- Heavy work performed on the UI isolate
- Excessive logging during animations
The first rule of optimization is simple:
Measure before changing code.
Use Flutter's Performance Tools
Flutter DevTools provides tools for understanding CPU usage, memory, frame rendering, widget rebuilds, and network activity.
Run your application in profile mode when evaluating real performance.
flutter run --profile
Debug mode is useful for development but should not be used as the final benchmark.
Look for Expensive Work in the UI Isolate
Dart code executed on the main isolate can compete with UI work.
Avoid doing large computations directly inside a build method.
Bad example:
@override
Widget build(BuildContext context) {
final processed = expensiveCalculation(items);
return ListView(
children: processed.map(buildItem).toList(),
);
}
The calculation can execute whenever the widget rebuilds.
Move expensive work outside the build path.
final processed = calculateItems(items);
return ListView(
children: processed.map(buildItem).toList(),
);
For genuinely CPU-heavy operations, consider moving the computation to another isolate.
Use Isolates for CPU-Heavy Work
For expensive CPU-bound tasks, Dart provides isolate APIs.
final result = await Isolate.run(() {
return expensiveCalculation(input);
});
This can be useful for tasks such as:
- Image processing
- Large JSON transformations
- Encryption
- Parsing large datasets
- Complex calculations
The goal is to prevent CPU-heavy work from blocking UI responsiveness.
Reduce Widget Rebuilds
One common performance problem is rebuilding a large widget tree when only a small part changed.
Instead of rebuilding everything:
setState(() {
counter++;
});
structure the widget tree so that only the necessary section depends on the changing state.
State management libraries such as BLoC can also help by allowing targeted rebuilds.
BlocBuilder<CartBloc, CartState>(
buildWhen: (previous, current) {
return previous.total != current.total;
},
builder: (context, state) {
return Text('\$${state.total}');
},
)
Use const Widgets
Flutter can optimize constant widget instances.
const Text('Hello Flutter');
Prefer const constructors when the widget and its parameters are compile-time constants.
For example:
class EmptyState extends StatelessWidget {
const EmptyState({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('No items found'),
);
}
}
Using const everywhere is not a magic performance solution, but it is a useful part of a clean widget tree.
Optimize Long Lists
For large collections, prefer lazy builders.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ProductTile(item: items[index]);
},
)
Avoid constructing thousands of widgets at once with a large children list when the content can be built lazily.
For complex lists, also consider:
- Stable item keys where necessary
- Avoiding unnecessary nested scrolling
- Efficient item layouts
- Proper image caching
- Pagination for large datasets
Avoid Expensive Work in build()
The build() method should primarily describe the UI.
Avoid:
Widget build(BuildContext context) {
final json = jsonDecode(largeJsonString);
final sorted = sortLargeCollection(data);
return MyWidget(data: sorted);
}
Instead, perform those operations when the data changes and pass the prepared result to the UI.
Optimize Images
Large images can consume substantial memory and increase decoding work.
If a thumbnail is displayed at a small size, downloading a massive source image is inefficient.
Use appropriately sized assets or request resized images from your backend.
For local images, consider:
Image.asset(
'assets/images/product.png',
cacheWidth: 400,
)
The appropriate size depends on the target device and display density.
Be Careful with Opacity and Clipping
Some visual effects can be more expensive than simple painting, especially when combined with complex widget trees.
Instead of repeatedly wrapping large subtrees in expensive effects, consider whether the effect can be applied to a smaller widget.
Also avoid unnecessary clipping, shadows, and compositing layers in frequently animated areas.
Optimize Animations
Animations should avoid unnecessary work on every frame.
Keep animated regions small when possible.
Use Flutter's animation APIs rather than manually triggering frequent state updates.
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: expanded ? 300 : 100,
child: const Placeholder(),
)
Implicit animations can be a clean solution for straightforward transitions.
Watch for Excessive Logging
Logging large objects repeatedly during an animation or scrolling operation can affect performance.
Avoid code such as:
print(largeResponseObject);
inside frequently executed callbacks.
Use structured logging and reduce verbose logging in release builds.
Measure Again After Optimization
Performance optimization should be iterative:
Measure
↓
Identify bottleneck
↓
Change one thing
↓
Measure again
↓
Compare results
Without measurement, it is easy to optimize code that was never a real bottleneck.
A Practical Performance Checklist
Before releasing a Flutter application, check:
- Profile performance on physical devices
- Inspect frame rendering in DevTools
- Avoid heavy work in
build() - Use isolates for CPU-heavy tasks
- Reduce unnecessary rebuilds
- Use lazy list builders
- Optimize image dimensions
- Avoid excessive compositing and clipping
- Keep animations lightweight
- Avoid excessive logging
- Test on lower-end devices
Conclusion
Flutter performance problems are usually easier to solve when you treat them as measurement problems rather than guessing games.
Start with profiling, identify the slow operation, make a focused change, and measure again. With efficient widget trees, lazy lists, optimized images, targeted rebuilds, and isolates for expensive CPU work, you can eliminate many common sources of jank and deliver a much smoother Flutter experience.
Useful Links
Website: www.v-modal.com
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)