npx skills add ...
npx skills add zakariaf/flutter-skills --skill 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.
npx skills add zakariaf/flutter-skills --skill 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.
const everything legal. A const subtree is skipped on rebuild — the framework short-circuits it by identity. Keep prefer_const_constructors on as an error; a non-const literal that could be const is a lint failure, not a preference.ref.watch(provider) for a whole state object when one field changed — watch provider.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.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.build(). No jsonDecode, sort, regex, DateTime math, file/network, or large allocation — build() may run every frame. Compute in the Notifier/ViewModel once and cache the result in immutable state.compute() or a spawned Isolate. Blocking the UI thread is guaranteed jank; the raster thread cannot save you.cacheWidth/cacheHeight or ResizeImage, correct asset resolutions, precacheImage for above-the-fold art. Decoding a 4000 px source into a 100 px box spikes memory and OOMs cheap phones.ColoredBox/DecoratedBox over Container, SizedBox for spacing, FadeTransition/AnimatedOpacity over the Opacity widget in hot paths. Avoid ClipPath/saveLayer inside scrolling lists — they force an offscreen buffer on the raster thread.RepaintBoundary around 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.child:. AnimatedBuilder/ValueListenableBuilder/StreamBuilder rebuild only the builder; hand the unchanging subtree in via child: so it is built once, not per frame.AnimationControllers, gesture recognizers, stream subscriptions, timers, TextEditingControllers, caches — released in dispose() (widgets) or ref.onDispose (providers). autoDispose scoped session state so it does not survive the screen.saveLayer/blur/clip cost shows there, not in Dart. Never ship a frame-budget number that was asserted, not measured.Watch the single field that changes, not the whole state object:
.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.
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.
flutter run --profile on a physical floor device..select, add const, move work out of build().Opacity/clips/saveLayer, add a RepaintBoundary, simplify shaders.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/DateTime math/regex in build() — it may run every frame; precompute in the ViewModel.Opacity widget for fades in scrolling lists — forces saveLayer; use FadeTransition or fade via color.compute.RepaintBoundary on everything — layer-memory bloat; use it surgically around independently-repainting subtrees.AnimatedBuilder/StreamBuilder rebuilding an expensive child instead of passing it via child:.Paint/Path inside paint() — per-frame garbage; hold them as fields.const applied everywhere legal; prefer_const_constructors clean..select; a tick in one widget never rebuilds a heavy sibling..builder/slivers).build(); big work runs via compute/Isolate.cacheWidth/ResizeImage); above-the-fold precached.RepaintBoundary used surgically; expensive children passed via child:.CustomPainter allocates nothing in paint(); shouldRepaint is one value compare.autoDisposed; no leaks.state-management-riverpod — the watch/read/listen split, .select, family + autoDispose, immutable state that makes .select correct.widget-composition — small const Widget classes over _buildX methods, dispose discipline, cheapest-widget choices.custom-canvas-and-gestures — zero-allocation paint(), shouldRepaint as one value compare, repaint: Listenable.testing-strategy — clock-injected pure core so heavy compute is testable off the UI isolate.// 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 — 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);// parseItems is a top-level or static function (isolate entry-point rules).
final items = await compute(parseItems, jsonString);