Back to Home

Flutter Optimization: avoid rebuilds

The article analyzes 8 common Flutter app optimization errors: extra rebuilds, lack of const, logic in build(), incorrect lists. Code examples of fixes and key practices for stable 60 FPS are provided.

Flutter without freezes: 8 key optimizations
Advertisement 728x90

Practical Techniques for Speeding Up Flutter Apps

Flutter apps often suffer from stutters due to unnecessary rebuilds, operations on the UI thread, and inefficient list handling. This article breaks down key mistakes and how to fix them: from localizing updates to proper use of builders and animations. These techniques help maintain 60 FPS even on low-end devices.

Minimizing Unnecessary UI Rebuilds

Unnecessary rebuilds are a primary cause of performance drops. setState() rebuilds the entire subtree of a StatefulWidget, including heavy lists and images. This leads to layout recalculations and FPS drops.

Problematic Code:

Google AdInline article slot
class MyScreen extends StatefulWidget {
  @override
  State<MyScreen> createState() => _MyScreenState();
}
class _MyScreenState extends State<MyScreen> {
  int counter = 0;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Counter: $counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              counter++; // rebuilds the ENTIRE tree
            });
          },
          child: Text('Increment'),
        ),
      ],
    );
  }
}

Solution: ValueListenableBuilder updates only the dependent part of the UI.

class MyScreen extends StatelessWidget {
  final ValueNotifier<int> counter = ValueNotifier(0);
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ValueListenableBuilder<int>(
          valueListenable: counter,
          builder: (_, value, __) {
            return Text('Counter: $value');
          },
        ),
        ElevatedButton(
          onPressed: () => counter.value++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Similarly, StreamBuilder, Selector from provider, or BlocBuilder work. The repaint area is minimized, reducing CPU load.

Using const Constructors

Without const, each rebuild creates new widget objects, straining the GC and CPU. In lists with hundreds of icons, texts, and paddings, this is critical.

Google AdInline article slot

Optimal approach:

Widget build(BuildContext context) {
  return const Column(
    children: [
      Text('Hello'),
      Icon(Icons.home),
      Padding(
        padding: EdgeInsets.all(8),
        child: Text('World'),
      ),
    ],
  );
}

Flutter reuses const objects, reducing allocations. The gain is noticeable in complex UIs with thousands of small widgets.

Moving Logic Out of build()

Sorting, filtering, or parsing in build() runs on every call—up to 60 times per second. For 60 FPS, only 16 ms per frame is available; heavy operations cause jank.

Google AdInline article slot

Correct approach:

late List<int> filtered;
@override
void initState() {
  super.initState();
  final sorted = [...items]..sort((a, b) => a.compareTo(b));
  filtered = sorted.where((e) => e > 10).toList();
}
@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: filtered.length,
    itemBuilder: (_, i) => Text('${filtered[i]}'),
  );
}

Logic runs once in initState or the business layer. build() remains declarative.

Lazy Lists with ListView.builder

ListView with children: [...] creates all items at once, even invisible ones. With 100+ items, memory and initial render suffer.

Recommendation:

Widget build(BuildContext context) {
  return ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      final item = items[index];
      return ListTile(
        title: Text(item.title),
        subtitle: Text(item.subtitle),
      );
    },
  );
}

Items are generated as you scroll, memory is optimized, and recycling works efficiently.

Keys for List Stability

Without a key, Flutter cannot match elements during list updates, causing unnecessary rebuilds and visual artifacts.

Critical for:

  • Reordering elements
  • Deletion/insertion
  • Transition animations
ListView.builder(
  itemCount: items.length,
  itemBuilder: (_, index) {
    final item = items[index];
    return ListTile(
      key: ValueKey(item.id),
      title: Text(item.title),
    );
  },
);

ValueKey(item.id) ensures correct updates.

Animations Without setState

Timer.periodic with setState every 16–100 ms overloads the UI thread.

Standard tool:

late AnimationController controller;
@override
void initState() {
  super.initState();
  controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 2),
  )..repeat();
}
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: controller,
    builder: (_, __) {
      return LinearProgressIndicator(value: controller.value);
    },
  );
}

AnimationController syncs with the device's FPS, updating minimal UI.

Breaking Down into Small Widgets

One huge StatefulWidget rebuilds entirely on any change.

Advantages of composition:

  • Localized rebuilds
  • Code reuse
  • Better readability and testing
Widget build(BuildContext context) {
  return Column(
    children: const [
      Header(),
      Expanded(child: ItemsList()),
      Footer(),
    ],
  );
}
class Header extends StatelessWidget {
  const Header();
  @override
  Widget build(BuildContext context) {
    return Text('Header');
  }
}

Future Outside build()

FutureBuilder with future: fetchData() in build() launches requests on every rebuild.

late Future dataFuture;
@override
void initState() {
  super.initState();
  dataFuture = fetchData();
}
Widget build(BuildContext context) {
  return FutureBuilder(
    future: dataFuture,
    builder: (_, snapshot) {
      if (!snapshot.hasData) return CircularProgressIndicator();
      return Text(snapshot.data.toString());
    },
  );
}

Future is created once; UI reacts to changes.

Key Takeaways

  • Localize updates: Use ValueListenableBuilder, AnimatedBuilder instead of global setState.
  • const everywhere: Reduces allocations by 50–70% in complex UIs.
  • Logic in initState: build() is only for declarative description.
  • ListView.builder + keys: For stable dynamic lists.
  • Widget composition: Small StatelessWidgets over monoliths.

— Editorial Team

Advertisement 728x90

Read Next