Skill: Rendering Optimization
Overview
A Flutter frame has a budget of ~16ms at 60Hz and ~8ms at 120Hz, split across two threads: the UI thread (Dart: build, layout, paint recording) and the raster thread (GPU: turning the recording into pixels). Jank is always one of those two overrunning, and the fix differs completely between them — so measure first, in --profile mode on a real device. Debug-mode timings are meaningless; the assertions and unoptimized code alone can triple frame times.
Use Cases
- "The list stutters when I scroll."
- Dropped frames during a page transition or animation.
- Slow cold start.
- Memory growth or an out-of-memory crash on image-heavy screens.
- App size reduction before a release.
Best Practices
Diagnose before changing anything.
- Run
flutter run --profileon a physical device; use DevTools' Performance view. A tall bar on the UI thread means too much Dart work (rebuilds, layout); a tall bar on the raster thread means expensive painting (saveLayer, shaders, large images). - Turn on "Track widget builds" in DevTools to see which widgets rebuild and how often. A widget rebuilding 60×/second that does not need to is the single most common cause.
debugPrintRebuildDirtyWidgetsand the Flutter Inspector's repaint-rainbow give the same signal in a pinch.
Cut rebuilds (UI thread).
constconstructors everywhere they apply — aconstwidget is skipped entirely on rebuild. Enableprefer_const_constructorsin the analyzer so this is enforced, not remembered.- Watch the narrowest slice:
ref.watch(cartProvider.select((c) => c.itemCount))rebuilds on the count, not the whole cart. - Scope a
Consumerto the smallest subtree that actually depends on the provider, instead of making the whole screen aConsumerWidget. - Pass an unchanging subtree as the
child:argument ofAnimatedBuilder/ValueListenableBuilderso it is built once and reused every frame. - Give list items stable keys so element identity survives reordering.
Cut paint cost (raster thread).
ListView.builder(never aColumninside aSingleChildScrollViewfor long lists), withitemExtentorprototypeItemwhen rows are uniform — it lets Flutter skip per-item layout.RepaintBoundaryaround a subtree that animates independently, so its repaints don't dirty the rest of the screen. Do not sprinkle it everywhere; each boundary costs a layer.Opacity,ClipRRect, andShaderMaskover a large subtree triggersaveLayer, which is expensive. Prefer a color with alpha, aborderRadiuson the decoration, or a pre-clipped asset.IntrinsicHeight/IntrinsicWidthcan be O(n²) on nested layouts — avoid in list items.
Images and memory.
- Decode at display size.
cacheWidth/cacheHeight(orResizeImage) — decoding a 4000px photo into a 100px avatar is the most common memory bug in Flutter apps, and it is invisible until the OOM. cached_network_imagefor remote images; precache only what the first frame needs.
Heavy work and startup.
- Move CPU-bound work (large JSON parses, image processing, crypto) to an isolate with
Isolate.run/compute. Anything over a few milliseconds on the UI thread is a dropped frame. - Keep
main()lean: do not block the first frame on network or database warm-up. Let providers resolve lazily and render a skeleton. flutter build --analyze-sizeto find what is actually in the bundle; ship--obfuscate --split-debug-infoand upload the symbol files.
Renderer note. Flutter's renderer has moved from Skia to Impeller, whose rollout differs by platform and version. Impeller precompiles shaders, which makes the older "SkSL shader warm-up" advice obsolete where it is active. Check which renderer your target Flutter version uses on each platform before applying shader-jank guidance.
Anti-Patterns
- ❌ Optimizing from a debug-mode profile, or from a guess.
- ❌ A
Columnof hundreds of children inside aSingleChildScrollView. - ❌ Full-resolution image decode for a thumbnail.
- ❌
setState/notifier updates that rebuild an entire screen for one changed field. - ❌ JSON parsing or crypto on the UI isolate.
- ❌
RepaintBoundaryscattered as a cargo-cult fix without a measurement. - ❌
Opacitywrapping a large animated subtree. - ❌ Blocking the first frame on network or DB initialization.
Checklist
- [ ] Measured in
--profileon a real device before and after. - [ ] Attributed to the UI thread or the raster thread, and the fix matches.
- [ ]
constconstructors used;prefer_const_constructorsenabled. - [ ] Provider watches narrowed with
select;Consumerscoped tightly. - [ ] Long lists use
ListView.builderwithitemExtent/prototypeItemwhere uniform. - [ ] Images decoded at display size.
- [ ] CPU-bound work runs in an isolate.
- [ ] Startup does not block the first frame on I/O.
- [ ] Release builds obfuscated with symbols uploaded.
Dart Examples
// ❌ rebuilds the whole screen when any cart field changes
final cart = ref.watch(cartProvider);
return Text('${cart.itemCount}');
// ✅ rebuilds only when itemCount changes
final count = ref.watch(cartProvider.select((c) => c.itemCount));
return Text('$count');// ✅ the expensive subtree is built once and reused each animation frame
AnimatedBuilder(
animation: controller,
// `child` is built once, outside the per-frame builder.
child: const ExpensiveStaticHeader(),
builder: (context, child) => Opacity(opacity: controller.value, child: child),
);// ✅ uniform rows: itemExtent lets Flutter skip per-item layout during scroll
ListView.builder(
itemExtent: 72,
itemCount: articles.length,
itemBuilder: (context, i) => RepaintBoundary(
child: ArticleRow(key: ValueKey(articles[i].id), article: articles[i]),
),
);// ❌ decodes at full resolution ✅ decodes at display size
Image.network(url, width: 48) // Image.network(url, width: 48, cacheWidth: 96)// ✅ a large parse belongs off the UI isolate
final articles = await Isolate.run(() => parseArticles(rawJson));Common Interview Questions
- How do you tell UI-thread jank from raster-thread jank, and why does the distinction matter?
- Why must performance be measured in profile mode rather than debug?
- What does a
constconstructor actually save during a rebuild? - What does
RepaintBoundarycost, and when is it worth it? - Why is
Opacityover a large subtree expensive? - What is the most common cause of image-related OOM in Flutter, and the fix?
- When should work move to an isolate?
AI Implementation Notes
- Never propose an optimization without naming the measurement that justifies it.
- Default generated lists to
ListView.builderwith stable keys; additemExtentwhen uniform. - Always set
cacheWidth/cacheHeightwhen generating image widgets with a known display size. - Prefer
selectover watching a whole state object in generated widgets. - Verify the renderer default (Impeller vs Skia) for the project's Flutter version before giving shader-warm-up advice.
- iOS counterparts:
../ios/startup_optimization.md,../ios/memory_optimization.md. - Related:
../../../checklists/performance_review.md,../../architecture/flutter/state_management.md.