How to Stop Useless Widget Rebuilds in Flutter

How to Stop Useless Widget Rebuilds in Flutter

Slow apps feel cheap. The most common source of slowness in Flutter is not a complex algorithm or slow database query. It is something much simpler. Your app is probably just rebuilding parts of its UI that have not changed.

Flutter’s declarative model is powerful. You describe what the UI should look like for a given state and the framework handles the rest. But this power comes with a footgun. If you are not careful you will tell Flutter to rebuild everything on every small change. This wastes CPU cycles and causes dropped frames. The good news is that fixing this is usually easy. It just requires discipline.

The Problem Starts with setState

The most basic way to manage state in Flutter is with a StatefulWidget and a call to setState. When you call setState you are telling Flutter that some state has changed and it needs to rebuild the widget. The problem is that it does not just rebuild the one tiny part of the widget that needs to change. It rebuilds the entire widget returned by the build method and all its children.

Imagine a screen with a header a list of items and a floating action button. If the state for a single list item is held in the main screen widget a tap on that item could trigger a setState call that rebuilds the header the button and every other item in the list.

// A common but inefficient pattern
class BigScreen extends StatefulWidget {
  @override
  _BigScreenState createState() => _BigScreenState();
}

class _BigScreenState extends State<BigScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This entire build method runs on every increment
    // including the AppBar and other static widgets.
    return Scaffold(
      appBar: AppBar(title: Text(“Inefficient Rebuilds”)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('This text does not need to change:'),
            Text(
              '$_counter', // Only this widget needs the state
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

In this example the AppBar and the first Text widget are rebuilt every time the button is pressed. This is a waste.

First Fix Use const

The simplest fix for unnecessary rebuilds is to use the const keyword. When you declare a widget as a const you are telling Dart that this widget and its entire subtree will never change. Flutter’s build process is smart enough to see this and it will skip rebuilding that widget entirely.

You should be aggressive with const. Any widget that does not depend on changing state can and should be a const. This includes things like SizedBox Padding Icon and Text with static content.

// A slightly better version
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text(“Slightly Better”)), // Const here
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // This text is now constant and will not be rebuilt
          const Text('This text does not need to change:'),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.headline4,
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _increment,
      child: const Icon(Icons.add), // Const here too
    ),
  );
}

This is a good first step. It is like free performance. But the core problem remains. The Column and Center widgets are still rebuilding.

Second Fix Push State Down

The real solution is to move your state as close as possible to the widget that uses it. Instead of having one large StatefulWidget that manages everything you should have many small StatefulWidgets that manage their own piece of state.

Let’s refactor our counter example. We can extract the counter text into its own StatefulWidget.

// The main screen widget can now be stateless
class BetterScreen extends StatelessWidget {
  const BetterScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(“Efficient Rebuilds”)),
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('This text does not need to change:'),
            // We've extracted the changing part
            CounterText(),
          ],
        ),
      ),
    );
  }
}

// This new widget manages its own state
class CounterText extends StatefulWidget {
  const CounterText({super.key});

  @override
  _CounterTextState createState() => _CounterTextState();
}

class _CounterTextState extends State<CounterText> {
  int _counter = 0;

  void _increment() {
    // A timer to simulate increments
    Future.delayed(const Duration(seconds: 1), () {
      if (mounted) {
        setState(() {
          _counter++;
        });
        _increment(); // Loop
      }
    });
  }

  @override
  void initState() {
    super.initState();
    _increment();
  }

  @override
  Widget build(BuildContext context) {
    // Now only this Text widget rebuilds
    return Text(
      '$_counter',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

Now the main screen is a StatelessWidget. It will be built once and never again. The CounterText widget manages its own state. When its setState is called only that small Text widget is rebuilt. The AppBar the Column and the other Text widget are untouched. This principle of pushing state down is the most important concept for writing performant Flutter apps.

A Better Tool ValueNotifier

Pushing state down into smaller widgets works well. But sometimes you need to share state between widgets or you want to avoid creating many small StatefulWidget classes. For these cases ValueNotifier is a simple and powerful tool that comes with Flutter.

A ValueNotifier holds a single value. When the value changes it notifies any listeners. You can use a ValueListenableBuilder widget to listen to a ValueNotifier and rebuild a small part of your UI whenever the value changes.

This gives you very precise control over what gets rebuilt.

// Final version using ValueNotifier
class NotifierScreen extends StatelessWidget {
  NotifierScreen({super.key});

  // The state is now held in a ValueNotifier
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);

  void _increment() {
    _counter.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text(“ValueNotifier Demo”)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('This text does not need to change:'),
            // This builder listens to the notifier
            ValueListenableBuilder<int>(
              valueListenable: _counter,
              builder: (context, value, child) {
                // This part of the tree rebuilds when _counter changes
                return Text(
                  '$value',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Here the entire screen is a StatelessWidget. The state is held in the _counter notifier. The ValueListenableBuilder subscribes to this notifier and its builder function is the only thing that runs again when the value changes. This is often the cleanest and most performant solution for simple local state.

For more complex applications where you need to share state across many screens you might reach for a package like Provider or Riverpod. But they are built on the same underlying principles. If you understand how to use const push state down and use ValueNotifier you already know everything you need to keep your app fast.

Think about the parts of your own UI that change. Are you rebuilding the whole screen for a small update? Take a look at your own app and see where the rebuilds are happening. Why not try answering this for yourself.

— Rishi Banerjee
September 2025