Performance
Rendering pipeline, jank, memory leaks, profiling tools
- 01
What causes jank in Flutter and how do you optimize performance?
HardJank is a frame that misses the display's budget — 16ms at 60Hz, 8ms at 120Hz — so the frame is dropped and the scroll or animation visibly stutters.
// ── Const constructors — zero cost reuse ─────────── // ❌ Creates new TextStyle object every build Text('Hello', style: TextStyle(fontSize: 16, color: Colors.blue)); // ✅ Reuses same instance const Text('Hello', style: TextStyle(fontSize: 16, color: Colors.blue)); … - 02
How do you detect and fix memory leaks in Flutter?
HardA leak in Flutter is almost always a disposable you forgot to dispose, or a long-lived object still holding a reference to a dead widget's State.
// ── Common leaks and fixes ───────────────────────── class LeakyWidget extends StatefulWidget { @override State<LeakyWidget> createState() => _LeakyState(); } … - 03
How do you profile a Flutter app and read DevTools timeline correctly?
MediumPerformance work without measurement is folklore.
# 1) Run in profile mode on a real device flutter run --profile -d <device_id> # 2) Open DevTools — flutter run prints a URL like: # The Flutter DevTools debugger and profiler is available at: ... … - 04
How do you cut unnecessary rebuilds — const, selector pattern, RepaintBoundary?
MediumMost jank in Flutter apps is caused by widgets rebuilding when they didn't need to.
// ── 1) const everywhere const works ─────────────── // ❌ Creates a fresh TextStyle each build Text('Hello', style: TextStyle(fontSize: 16, color: Colors.blue)); // ✅ Const, identity-equal, skipped on rebuild const Text('Hello', style: TextStyle(fontSize: 16, color: Colors.blue)); … - 05
How do you optimize image rendering — sizing, caching, and codecs?
MediumImages are the single biggest performance lever for content-heavy apps.
// ── Right-sized cache decode ─────────────────────── Image.network( product.imageUrl, width: 200, height: 200, fit: BoxFit.cover, cacheWidth: 400, cacheHeight: 400, // ✅ decode once at the size you use ); … - 06
When does Opacity / BackdropFilter / shadows hurt, and how do you fix it?
HardSome Flutter widgets force expensive raster work — they look harmless but turn the GPU into the bottleneck.
// ── Opacity — prefer color alpha for static content ─ // ❌ saveLayer per build, expensive Opacity(opacity: 0.5, child: const Text('disabled')); // ✅ shader-level blend, no offscreen buffer Text('disabled', style: TextStyle(color: Colors.black.withValues(alpha: 0.5))); … - 07
Why did Skia need SkSL shader warm-up, and what changed when Impeller became the default?
MediumImpeller is Flutter's renderer on iOS and Android today; Skia is the older backend it replaced, and is still what Flutter web's CanvasKit is built on.
# Which backend is this build really using? The engine logs it at startup. flutter run --profile -d <device> # e.g. "Using the Impeller rendering backend (Vulkan)" # A/B a suspected Impeller regression by turning it off for one run flutter run --profile --no-enable-impeller -d <device> … - 08
How do you keep startup time low — splash, deferred work, deferred components?
HardTime-to-interactive on cold start is one of the most-watched mobile KPIs.
// Lean main() — hydrate what the first frame needs, nothing else Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); // ✅ Quick local hydration — under 50ms typically final prefs = await SharedPreferences.getInstance(); … - 09
How do you build a long, smooth scrolling list without jank?
MediumThe performance gap between a hand-rolled list and a properly-built one is enormous.
class FeedPage extends StatelessWidget { const FeedPage({super.key, required this.posts}); final List<Post> posts; @override Widget build(BuildContext context) { … - 10
The app download is 90 MB and product wants it halved. How do you find the weight and cut it?
MediumStart from a size snapshot rather than intuition: build the release artifact with size analysis on and open the result in the DevTools app size tool.
flutter build appbundle --release --analyze-size flutter build ipa --release --analyze-size # writes a .json snapshot; open it in DevTools -> App Size, and diff two of them flutter build apk --release --split-per-abi # only when not shipping a bundle flutter build apk --release \ … - 11
Your app is smooth on your device. How do you find out whether it is smooth for real users?
HardInstrument frame timings in the release build and aggregate them in the field, because your development device is the least representative phone your app will ever run on.
class JankReporter { JankReporter(this._analytics); final Analytics _analytics; final _slow = <String, int>{}; int _frames = 0; … - 12
An AnimationController is rebuilding a whole page sixty times a second. How do you scope the animation down?
MediumAn animation ticks on the UI thread, so the fix is to shrink what each tick touches until only the moving part is rebuilt and, better still, only repainted.
// ❌ every tick rebuilds the whole page class _PageState extends State<Page> with SingleTickerProviderStateMixin { late final _ctrl = AnimationController(vsync: this, duration: kThemeAnimationDuration) ..addListener(() => setState(() {})) ..repeat(); … - 13
Opening the keyboard rebuilds the whole screen. What is MediaQuery.of doing, and how do you narrow it?
MediumMediaQuery.of(context)subscribes the calling widget to the entireMediaQueryData, so while the keyboard slides up andviewInsetschanges, every widget that called it rebuilds on every frame of that animation.// ❌ one .of at the top of the page: everything below rebuilds on every // frame of the keyboard animation, and again on rotation class ProfilePage extends StatelessWidget { const ProfilePage({super.key}); @override … - 14
Your CustomPainter chart repaints on every ancestor rebuild even though its data never changed — what do you check first?
MediumshouldRepaintis the whole contract:CustomPaintbuilds a new painter on every rebuild, and the render object repaints unless the new painter says nothing it draws has changed.// ❌ repaints on every ancestor rebuild and allocates per frame class BadChart extends CustomPainter { BadChart(this.points); final List<Offset> points; @override … - 15
Pushing a details route drops frames for the first third of the transition. What is happening and how do you fix it?
MediumThe pushed route's first build, layout, paint and image decode all land in the frames that animate the transition, so blocking work in
initStateorbuildis taken straight out of the animation's budget.// ❌ blocking work inside the frames that animate the push class _BadDetailsState extends State<DetailsPage> { late final Order _order; @override void initState() { … - 16
How do you stop a frame-time regression from being merged, instead of discovering it after the release?
MediumScript the interaction, run it on a pinned physical device in profile mode from CI, summarise the frame timings, and fail the job when a percentile crosses an agreed threshold.
// integration_test/scroll_perf_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); … - 17
You moved JSON parsing into compute() and the frame drops got worse. Why can an isolate be slower than doing the work inline?
HardAn isolate is not free: spawning one costs about a millisecond on a phone, and everything you send into it is deep-copied, so a small payload can pay more in overhead than the parse ever cost.
// ❌ one isolate spawn per row, for a payload that parses in microseconds Future<List<Tag>> tagsFor(String json) => compute(parseTags, json); // ❌ Isolate.run over a closure that captures `this`: the enclosing object // and everything it references is copied into the new isolate Future<Report> summarise() => Isolate.run(() => _summarise(_rows)); … - 18
A list of cards wrapped in IntrinsicHeight stutters as soon as it grows. Why does layout suddenly cost so much?
HardFlutter's layout is one pass — constraints down, sizes up — and an intrinsic query breaks that by laying the subtree out speculatively before the real pass, so every intrinsic multiplies the cost of every layout under it.
// ❌ a speculative layout pass per item, on every layout of the list ListView.builder( itemCount: offers.length, itemBuilder: (context, i) => IntrinsicHeight( // O(N^2) in the worst case child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, … - 19
The timeline shows dropped frames lining up with GC events. What in the code causes that, and what do you change?
HardDart collects the young generation with a stop-the-world copying scavenger, so code that allocates on every frame forces those collections to run inside frames instead of between them.
// ❌ a new list, a new closure and a Paint per bar, sixty times a second AnimatedBuilder( animation: _controller, builder: (context, _) => CustomPaint( painter: _Bars( values: data.map((d) => d.value * _controller.value).toList(), … - 20
DevTools shows a 40 MB Dart heap, but the OS reports 500 MB and kills the app in the background. Where is the memory?
HardAlmost none of a Flutter app's footprint is Dart objects — decoded bitmaps, GPU textures, engine caches and plugin allocations are native memory that the Dart heap chart never shows.
void main() { // the default cap is 1000 images / 100 MB; too generous for low-end phones PaintingBinding.instance.imageCache ..maximumSize = 200 ..maximumSizeBytes = 40 << 20; runApp(const App()); …