agentsclimarketplace

Flutter performance

Skill zakariaf/Flutter-Skills/skills/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

Install
npx -y skills add zakariaf/Flutter-Skills --skill flutter-performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 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

  1. 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.
  2. Shrink the rebuild scope to the smallest changing widget. Never 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.
  3. 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.
  4. No expensive work in 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.
  5. Heavy CPU off the UI isolate. Parse large payloads, process images, crunch numbers in compute() or a spawned Isolate. Blocking the UI thread is guaranteed jank; the raster thread cannot save you.
  6. Size image decode to the display slot. 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.
  7. Prefer the cheapest widget that does the job. 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.
  8. 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.
  9. Pass expensive children through child:. AnimatedBuilder/ValueListenableBuilder/StreamBuilder rebuild only the builder; hand the unchanging subtree in via child: so it is built once, not per frame.
  10. Dispose to avoid leaks. 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.
  11. 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

  1. flutter run --profile on a physical floor device.
  2. DevTools -> Performance; record while reproducing the jank.
  3. For each over-budget frame, check whether the time is in Build, Layout, or Raster.
    • Build-heavy -> narrow rebuild scope with .select, add const, move work out of build().
    • Raster-heavy -> reduce Opacity/clips/saveLayer, add a RepaintBoundary, simplify shaders.
  4. Use "Track widget rebuilds" to find widgets rebuilding too often.
  5. 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/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.
  • 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.
  • 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:.
  • Rebuilding on every keystroke without debounce, or unbounded caches/listeners that leak.
  • Allocating Paint/Path inside paint() — per-frame garbage; hold them as fields.

Definition of done

  • const applied everywhere legal; prefer_const_constructors clean.
  • 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 via compute/Isolate.
  • Images sized to display (cacheWidth/ResizeImage); above-the-fold precached.
  • Cheapest suitable widgets/effects chosen; RepaintBoundary used surgically; expensive children passed via child:.
  • Any CustomPainter allocates nothing in paint(); shouldRepaint is 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 .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.

References

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most performance cost skills give in ~2.2k tokens

Counted across 797 of the 1,117 authors here whose files we hold, read 2026-09-06

  • Check for product marketing context firstin 46 of 797, across 20 files
  • Measure before optimizingin 31 of 797, across 25 files
  • Profile first to identify the actual bottleneckin 23 of 797, across 22 files
  • Verify your robots.txt allows AI crawlersin 21 of 797, across 12 files
  • Import directly and avoid barrel filesin 19 of 797, across 15 files
  • Spawn all runs in the same turnin 18 of 797, across 11 files
  • Write a draft of the skillin 17 of 797, across 10 files
  • Understand the user's intentin 17 of 797, across 10 files
  • Use React.cache for per-request deduplicationin 16 of 797, across 11 files
  • Profile before optimizingin 16 of 797, across 14 files
  • Include specific numbers with sourcesin 15 of 797, across 8 files
  • Add lazy loading to below-fold imagesin 15 of 797, across 10 files

Said here and by no other author read

  • Const everything legal.
  • Shrink rebuild scope to the smallest changing widget.
  • Use lazy builders for long lists.
  • Offload heavy CPU tasks to isolates.
  • Prefer the cheapest widget that does the job.
  • Use RepaintBoundary around costly repainting subtrees.

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.