Performance, images and animations

Find rebuilds that matter, keep the frame budget, animate implicitly and with Hero transitions, and serve images at the size the screen actually needs.

Rebuilds and const

// Bad: the whole list rebuilds when the counter changes
class BadPage extends StatefulWidget {
  const BadPage({super.key});
  @override
  State<BadPage> createState() => _BadPageState();
}

class _BadPageState extends State<BadPage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) => Column(
        children: [
          Text('$_count'),
          const HeavyChart(),          // const: skipped entirely
          _ExpensiveList(items: const ['a', 'b', 'c']),
        ],
      );
}

// Better: push the state down so only the label rebuilds
class CounterLabel extends StatefulWidget {
  const CounterLabel({super.key});
  @override
  State<CounterLabel> createState() => _CounterLabelState();
}

class _CounterLabelState extends State<CounterLabel> {
  int _count = 0;

  @override
  Widget build(BuildContext context) => TextButton(
        onPressed: () => setState(() => _count++),
        child: Text('$_count'),
      );
}
  • A const constructor lets Flutter skip the subtree rebuild entirely — it is the cheapest optimisation available.
  • Use RepaintBoundary around independently animating widgets so their repaints do not dirty the parent layer.
  • In the DevTools performance overlay, look for build times above a few milliseconds in the raster or UI thread during scrolling.
  • Never call setState on a large ancestor to update one small label.

Implicit animations and Hero

class ExpandingCard extends StatefulWidget {
  const ExpandingCard({super.key});
  @override
  State<ExpandingCard> createState() => _ExpandingCardState();
}

class _ExpandingCardState extends State<ExpandingCard> {
  bool _open = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _open = !_open),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 250),
        curve: Curves.easeOutCubic,
        height: _open ? 240 : 96,
        decoration: BoxDecoration(
          color: Theme.of(context).colorScheme.surfaceContainerHighest,
          borderRadius: BorderRadius.circular(_open ? 20 : 12),
        ),
        child: AnimatedOpacity(
          opacity: _open ? 1 : 0,
          duration: const Duration(milliseconds: 200),
          child: const Text('Details'),
        ),
      ),
    );
  }
}

// Hero works across routes with a matching tag
Hero(tag: 'avatar-${user.id}', child: CircleAvatar(backgroundImage: image))
NeedUseNote
Value change over timeAnimatedContainerImplicit, no controller
Sequenced timelineAnimationControllerDispose it explicitly
Shared element between screensHeroTags must be unique per route
Staggered list entryA controller with intervalsKeep under 400ms total

Images and the frame budget

Image.network(
  url,
  width: 320,
  height: 200,
  fit: BoxFit.cover,
  cacheWidth: (320 * MediaQuery.devicePixelRatioOf(context)).round(),
  filterQuality: FilterQuality.medium,
  frameBuilder: (context, child, frame, wasSync) =>
      wasSync ? child : AnimatedOpacity(opacity: frame == null ? 0 : 1, duration: const Duration(milliseconds: 200), child: child),
  errorBuilder: (context, error, stack) => const ColoredBox(color: Color(0x11000000)),
)
⚠️
Decoding a full-resolution photo for a 100px thumbnail wastes both memory and GPU time. Set cacheWidth or cacheHeight on every remote image so the decoded bitmap matches the on-screen size.

FAQ

What is the frame budget I should target?
16.6ms per frame for 60Hz and 8.3ms for 120Hz devices. Anything consistently above that shows up as jank when the user scrolls or drags.
Does Impeller change what I should optimise?
Impeller removes most shader-compilation jank, so the remaining cost is usually build time and oversized images. Start by reducing rebuilds and decoding images at the right size rather than tuning shaders.

Material 3 theming and adaptive UI CI/CD, flavors and store deployment

Last refreshed 2026-09-18.