Flutter: How to Stop Unwanted Widget Rebuilds

Unwanted widget rebuilds in Flutter waste CPU/GPU and cause jank. They happen when large parts of the tree update even though only a small value changed. The goal is to reduce rebuild scope and make builds cheap and side-effect-free.

Approaches

Approach A: Move work out of build

Keep build pure. Do expensive work once in lifecycle methods, not every frame.

import 'package:flutter/material.dart';

class ProfileCard extends StatefulWidget {
  const ProfileCard({super.key, required this.name});
  final String name;

  @override
  State<ProfileCard> createState() => _ProfileCardState();
}

class _ProfileCardState extends State<ProfileCard> {
  late final String greeting; // computed once

  @override
  void initState() {
    super.initState();
    greeting = 'Hello, ${widget.name}'; // ✅ not recomputed in build
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(greeting),
      ),
    );
  }
}

Use:

  • initState or didUpdateWidget for one-time or rare computations.
  • late final for memoized results.
  • Avoid async calls or setState inside build.

Approach B: Split widgets to isolate rebuilds

Lift static parts into separate widgets so they never rebuild.

class PageShell extends StatelessWidget {
  const PageShell({super.key, required this.counter});
  final int counter;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: const [
        _Sidebar(), // ✅ never rebuilds
        VerticalDivider(),
      ],
    );
  }
}

class _Sidebar extends StatelessWidget {
  const _Sidebar();
  @override
  Widget build(BuildContext context) {
    return SizedBox(width: 220, child: Text('Static menu'));
  }
}

Rule of thumb: small widgets, shallow rebuilds.
A small rebuild boundary beats clever caching.


Approach C: Use const aggressively

const widgets are canonicalized and skipped during diffing.

class CountView extends StatelessWidget {
  const CountView({super.key, required this.count});
  final int count;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Icon(Icons.timer),
        const SizedBox(width: 8),
        Text('$count'),
      ],
    );
  }
}

Make constructors const, mark child trees const, and prefer const literals everywhere.


Approach D: Limit rebuild scope with selectors or buildWhen

Only rebuild the widgets that depend on the changed slice of state.

Provider (Selector):

class Cart extends ChangeNotifier {
  int _total = 0;
  int get total => _total;
  void add(int price) {
    _total += price;
    notifyListeners();
  }
}

class CartTotalChip extends StatelessWidget {
  const CartTotalChip({super.key});

  @override
  Widget build(BuildContext context) {
    return Selector<Cart, int>(
      selector: (_, cart) => cart.total,
      builder: (_, total, __) => Chip(label: Text('Total: \$${total}')),
    );
  }
}

Bloc (buildWhen):

BlocBuilder<CartCubit, CartState>(
  buildWhen: (prev, next) => prev.total != next.total,
  builder: (_, state) => Text('Total: ${state.total}'),
);

Riverpod (select):

final cartProvider = StateNotifierProvider<CartNotifier, Cart>((ref) => ...);

class CartTotalText extends ConsumerWidget {
  const CartTotalText({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final total = ref.watch(cartProvider.select((c) => c.total));
    return Text('Total: $total');
  }
}

Approach E: Use child parameters to freeze subtrees

Some builders let you pass a static child that won’t rebuild.

class LikeButton extends StatelessWidget {
  const LikeButton({super.key, required this.count});
  final int count;

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 200),
      child: Text('$count', key: ValueKey(count)),
    );
  }
}

class CardWithStaticHeader extends StatelessWidget {
  const CardWithStaticHeader({super.key, required this.builder});
  final Widget Function(BuildContext) builder;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const _Header(), // ✅ static
        builder(context),
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();
  @override
  Widget build(BuildContext context) => const Text('My Header');
}

Also available in Consumer, BlocBuilder, and LayoutBuilder.


Approach F: Lists — prefer builder constructors and keys

Avoid rebuilding long lists; let Flutter lazily build visible items.

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return _ItemTile(
      key: ValueKey(items[index].id),
      item: items[index],
    );
  },
);
  • Use ListView.builder or GridView.builder.
  • Provide stable Keys.
  • Avoid ListView(children: [...]) for large lists.

Approach G: Implement shouldRepaint / shouldRebuild

Stop unnecessary redraws and rebuilds by comparing inputs.

class RingPainter extends CustomPainter {
  RingPainter(this.color);
  final Color color;

  @override
  void paint(Canvas canvas, Size size) {/*...*/}

  @override
  bool shouldRepaint(covariant RingPainter old) => old.color != color;
}

For sliver delegates:

SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Text('$index'),
    addAutomaticKeepAlives: false,
  ),
);

Approach H: Use RepaintBoundary for paint isolation

RepaintBoundary reduces paint cost of complex areas.
It doesn’t stop rebuilds but improves rendering performance.

const RepaintBoundary(
  child: ComplexChart(),
);

Decision Guide

  • Big widget rebuilding on tiny change? Split widgets and add const.
  • State management causing full-tree rebuilds? Use selectors or buildWhen.
  • List feels slow? Use ListView.builder and keys.
  • Heavy computation in build? Move to initState or memoize.
  • Custom graphics? Implement shouldRepaint or add RepaintBoundary.

Trade-offs:

  • More widgets/files = clearer structure but more boilerplate.
  • Selectors = precision control, but extra code.
  • Keys = fix identity, but must remain stable.

Recommendation

  1. Keep build pure — no logic or async calls.
  2. Split widgets and use const everywhere.
  3. Use selectors (Selector, select, buildWhen) to target updates.
  4. Profile with the Flutter Performance Overlay and DevTools to verify gains.

References



Leave a Reply

Your email address will not be published. Required fields are marked *