Flutter Lesson 70 of 83 5 min read
Flutter Performance: Building Smooth, Fast Apps
Make Flutter apps fast: const widgets, small rebuilds, lazy lists, image sizing, isolates, and finding slow frames with DevTools.
On this page
Flutter is fast by default. A smooth app draws 60 or 120 frames a second, which leaves 16 or 8 milliseconds for each frame. When a frame takes longer, the app stutters. This is called jank. Nearly all jank comes from a small number of mistakes.
Measure in profile mode on a real device #
Debug mode is slow on purpose: it runs unoptimised code with extra checks. Emulators are not representative either.
flutter run --profile
Then open DevTools and its Performance view. Each frame is a bar, and frames over budget are highlighted. Select one to see whether the time went on build, layout, paint or raster.
Never judge performance in debug mode.
1. Use const #
A const widget is created once and Flutter skips rebuilding it entirely.
// Rebuilt every time the parent rebuilds
Padding(padding: EdgeInsets.all(16), child: Text('Total'))
// Built once
const Padding(padding: EdgeInsets.all(16), child: Text('Total'))
Enable the prefer_const_constructors lint and run dart fix --apply. Give your own widgets const constructors.
2. Rebuild less #
setState rebuilds the whole build method of that state object and everything below it. Keep that area small.
Push state down. Put the stateful part in its own small widget.
// Before: tapping the heart rebuilds the entire product page.
class ProductPage extends StatefulWidget { ... }
// After: only the heart rebuilds.
class ProductPage extends StatelessWidget {
const ProductPage({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
ProductImages(),
ProductDescription(),
FavouriteButton(), // a small StatefulWidget
],
);
}
}
Widgets, not helper methods. A method such as _buildHeader() runs again on every rebuild. A separate widget class with a const constructor can be skipped.
Listen narrowly. With Provider, use context.select. With Riverpod, ref.watch(provider.select(...)). With Bloc, buildWhen. Wrap only the part that changes in a ValueListenableBuilder or Consumer.
Use the child parameter of builders for the part that does not change:
AnimatedBuilder(
animation: _controller,
child: const ExpensiveWidget(), // built once
builder: (context, child) => Transform.rotate(angle: _controller.value * 6.28, child: child),
)
3. Build lists lazily #
// Builds all 5,000 rows at once
ListView(children: [for (final item in items) ItemTile(item)])
// Builds only what is visible
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(items[i]),
)
Also:
- Avoid
shrinkWrap: trueon long lists. It forces every item to be built. - Do not put a
ListViewinside aSingleChildScrollView. Use aCustomScrollViewwith slivers. - Set
itemExtentorprototypeItemwhen rows share a height.
4. Keep build fast and pure #
build may run sixty times a second. It should do nothing except return widgets.
// Wrong: sorts on every rebuild
Widget build(BuildContext context) {
final sorted = [...items]..sort((a, b) => a.name.compareTo(b.name));
return ListView(...);
}
Compute once when the data changes, in the state holder or in initState, and store the result. Never start network requests, read files or create controllers inside build.
5. Size your images #
Images are the largest memory users, and oversized ones cause stutter while scrolling.
Image.network(
url,
width: 120,
height: 120,
fit: BoxFit.cover,
cacheWidth: 240, // decode at the size shown (about 2x for sharp screens)
)
- Request thumbnails from the server instead of full-size photos.
- Use
cached_network_imageto avoid downloading again. - Call
precacheImagefor an image you are about to show, to avoid a flash.
6. Avoid expensive painting #
Some effects force Flutter to draw to an off-screen buffer first, which is costly.
| Costly | Cheaper |
|---|---|
Opacity around a large subtree, especially when animated | AnimatedOpacity or FadeTransition. For a colour, set the alpha in the colour itself |
ClipRRect around many items | borderRadius in a BoxDecoration |
BackdropFilter (blur) over large areas | Use sparingly, on small regions |
Many large BoxShadows | Fewer and smaller, or Card elevation |
7. RepaintBoundary #
When one small part of the screen repaints constantly, such as a spinner or an animation, wrap it in a RepaintBoundary so the rest is not repainted with it.
RepaintBoundary(child: const AnimatedLogo())
Check with “Highlight repaints” in the inspector. Do not add boundaries everywhere, since each one uses memory.
8. Move heavy work off the UI thread #
Dart runs your code on one thread. A long calculation there freezes the interface. Parsing a large JSON document, processing an image or sorting a huge list belongs in an isolate.
import 'package:flutter/foundation.dart';
final products = await compute(parseProducts, responseBody);
async does not help here. An async function still runs on the same thread; it only helps with waiting.
9. Dispose and cancel #
Controllers, timers and stream subscriptions that are never cleaned up keep running and keep memory alive. Dispose them in dispose().
10. Start-up time #
- Do as little as possible before
runApp. Show the first screen, then load. - Do not
awaitseveral slow initialisations one after another. Run them together withFuture.wait. - Defer features the user may not open.
App size #
flutter build apk --analyze-size
flutter build appbundle # Google Play then delivers only what each device needs
- Remove unused packages and assets.
- Compress images, or use WebP and SVG.
- Use
--split-debug-infoand--obfuscatefor release builds.
Do not optimise blindly #
Most code is fast enough. Write clear code first. When something feels slow, measure, find the one cause, fix it and measure again. Scattering RepaintBoundary and micro-optimisations through an app that has no measured problem only makes the code harder to read.
Try it yourself #
Build a list of 2,000 items with an image and text using a plain ListView with all children, and scroll it in profile mode with the Performance view open. Then switch to ListView.builder, add const where possible, set cacheWidth on the images, and compare the frame chart.