Flutter performance
Enforces Flutter runtime performance — const subtrees, minimal rebuild scope via ref.watch(select), lazy ListView/GridView builders and slivers, sized image decode (cacheWidth/ResizeImage), heavy work off the UI isolate via compute/Isolate, surgical RepaintBoundary, dispose everything, and measurement in profile mode on a floor device. Use when optimizing UI, diagnosing jank or dropped frames, tuning long lists or images, reviewing rebuild/repaint scope, or when the task mentions const, select, ListView.builder, cacheWidth, compute, RepaintBoundary, AnimatedBuilder, DevTools, raster thread, or 60/120fps.From its SKILL.md
npx -y skills add zakariaf/Flutter-Skills --skill flutter-performanceAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 19 days oldThe repository was created 19 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
9.7 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
Flutter Performance
Hold a steady 60 fps (<=16 ms/frame; <=8 ms on 120 Hz) on a floor device (low-end Android / older iPhone). Performance is a property you measure in profile mode, not one you assert. It comes from rebuilding less, painting cheaply, and keeping both the UI thread and the raster thread free.
Non-negotiable rules
consteverything legal. Aconstsubtree is skipped on rebuild — the framework short-circuits it by identity. Keepprefer_const_constructorson as an error; a non-const literal that could be const is a lint failure, not a preference.- Shrink the rebuild scope to the smallest changing widget. Never
ref.watch(provider)for a whole state object when one field changed — watchprovider.select((s) => s.field). A HUD counter tick must never rebuild a heavy sibling. Rebuild scope is the single biggest lever on build-thread cost. - Lazy everything long.
ListView.builder/GridView.builder/ slivers for variable or unbounded content.ListView(children: items.map(...).toList())builds every off-screen row up front — refuse it for anything not tiny and fixed. - No expensive work in
build(). NojsonDecode, sort, regex,DateTimemath, file/network, or large allocation —build()may run every frame. Compute in the Notifier/ViewModel once and cache the result in immutable state. - Heavy CPU off the UI isolate. Parse large payloads, process images, crunch numbers in
compute()or a spawnedIsolate. Blocking the UI thread is guaranteed jank; the raster thread cannot save you. - Size image decode to the display slot.
cacheWidth/cacheHeightorResizeImage, correct asset resolutions,precacheImagefor above-the-fold art. Decoding a 4000 px source into a 100 px box spikes memory and OOMs cheap phones. - Prefer the cheapest widget that does the job.
ColoredBox/DecoratedBoxoverContainer,SizedBoxfor spacing,FadeTransition/AnimatedOpacityover theOpacitywidget in hot paths. AvoidClipPath/saveLayerinside scrolling lists — they force an offscreen buffer on the raster thread. RepaintBoundaryaround costly, independently-repainting subtrees (an animation, a chart, a live indicator) so its repaint does not re-raster its neighbours. Do not sprinkle boundaries everywhere — each is a compositor layer that costs memory.- Pass expensive children through
child:.AnimatedBuilder/ValueListenableBuilder/StreamBuilderrebuild only thebuilder; hand the unchanging subtree in viachild:so it is built once, not per frame. - Dispose to avoid leaks.
AnimationControllers, gesture recognizers, stream subscriptions, timers,TextEditingControllers, caches — released indispose()(widgets) orref.onDispose(providers).autoDisposescoped session state so it does not survive the screen. - Measure in PROFILE mode on a real floor device with DevTools. Debug timings are meaningless (JIT, asserts). Watch the raster thread as well as the UI thread —
saveLayer/blur/clip cost shows there, not in Dart. Never ship a frame-budget number that was asserted, not measured.
Narrow the rebuild scope
Watch the single field that changes, not the whole state object:
// Rebuilds only when the count changes, not on every OrderState mutation.
class OrderBadge extends ConsumerWidget {
const OrderBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(orderProvider.select((s) => s.items.length));
return Text('$count');
}
}
.select runs the selector every publish but rebuilds only when the selected value's == changes, so its input must be an immutable value with real equality. See state-management-riverpod for the watch/read/listen split and family + autoDispose.
Keep expensive children out of the animation loop
// The card is built once; only the transform recomputes each tick.
AnimatedBuilder(
animation: _controller,
child: const ProductCard(),
builder: (_, child) =>
Transform.scale(scale: _controller.value, child: child),
);
Lazy lists and sized images
// Lazy — only visible rows are built.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ItemTile(item: items[i]),
);
// Decode to the slot, not the source resolution.
Image.asset('assets/hero.png', cacheWidth: 240);
Heavy work off the UI isolate
// parseItems is a top-level or static function (isolate entry-point rules).
final items = await compute(parseItems, jsonString);
Custom painting
For a CustomPainter, the discipline is zero-allocation paint(), a shouldRepaint that is one value comparison over an immutable scene value, and a repaint: Listenable (the AnimationController) instead of setState in a ticker. That treatment lives in custom-canvas-and-gestures; the one performance-critical rule to carry here: never allocate Paint/Path/Gradient inside paint() — hold them as painter fields and mutate cheap properties.
Profiling workflow
flutter run --profileon a physical floor device.- DevTools -> Performance; record while reproducing the jank.
- For each over-budget frame, check whether the time is in Build, Layout, or Raster.
- Build-heavy -> narrow rebuild scope with
.select, addconst, move work out ofbuild(). - Raster-heavy -> reduce
Opacity/clips/saveLayer, add aRepaintBoundary, simplify shaders.
- Build-heavy -> narrow rebuild scope with
- Use "Track widget rebuilds" to find widgets rebuilding too often.
- Fix one bottleneck, re-measure. Never optimize without a before/after measurement.
Anti-patterns
- Judging performance in debug mode — JIT and asserts make timings fiction. Profile mode, real device, always.
- Premature optimization with no profile data — adds complexity, hides nothing; measure first.
setState()high in the tree rebuilding a whole screen for a tiny change — scope the state down.ref.watch(provider)for a whole object in a leaf when one field changed — use.select.ListView(children: [...].toList())for long/unbounded content — builds every off-screen row.jsonDecode/sort/DateTimemath/regex inbuild()— it may run every frame; precompute in the ViewModel.Opacitywidget for fades in scrolling lists — forcessaveLayer; useFadeTransitionor fade via color.- Decoding full-resolution images into small widgets — memory spikes and OOM on cheap phones.
- Parsing large payloads / heavy loops on the UI isolate — frozen frames; use
compute. RepaintBoundaryon everything — layer-memory bloat; use it surgically around independently-repainting subtrees.AnimatedBuilder/StreamBuilderrebuilding an expensive child instead of passing it viachild:.- Rebuilding on every keystroke without debounce, or unbounded caches/listeners that leak.
- Allocating
Paint/Pathinsidepaint()— per-frame garbage; hold them as fields.
Definition of done
-
constapplied everywhere legal;prefer_const_constructorsclean. - Rebuilds scoped with
.select; a tick in one widget never rebuilds a heavy sibling. - Long/variable lists are lazy (
.builder/slivers). - No I/O, parsing, or heavy compute in
build(); big work runs viacompute/Isolate. - Images sized to display (
cacheWidth/ResizeImage); above-the-fold precached. - Cheapest suitable widgets/effects chosen;
RepaintBoundaryused surgically; expensive children passed viachild:. - Any
CustomPainterallocates nothing inpaint();shouldRepaintis one value compare. - Controllers, recognizers, subscriptions, timers disposed; scoped session state
autoDisposed; no leaks. - Measured in profile mode on a floor device via DevTools — UI and raster threads under budget; no frames over 16 ms; claim backed by a recording.
Related skills
state-management-riverpod— the watch/read/listen split,.select,family+autoDispose, immutable state that makes.selectcorrect.widget-composition— smallconstWidget classes over_buildXmethods, dispose discipline, cheapest-widget choices.custom-canvas-and-gestures— zero-allocationpaint(),shouldRepaintas one value compare,repaint:Listenable.testing-strategy— clock-injected pure core so heavy compute is testable off the UI isolate.
References
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.