DEV Community

Cover image for 🚫10 Flutter Performance Mistakes That Are Killing Your App
Hitesh Meghwal
Hitesh Meghwal

Posted on • Edited on

🚫10 Flutter Performance Mistakes That Are Killing Your App

Performance is crucial for mobile apps. A slow, laggy app will frustrate users and hurt your app store ratings. As Flutter developers, we sometimes unknowingly introduce performance bottlenecks that can significantly impact user experience.

In this post, I'll share 10 common Flutter performance mistakes I've encountered in real projects and how to fix them. These optimizations can make the difference between a smooth, responsive app and one that users immediately uninstall.

1. Building Widgets in the build() Method

The Mistake:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Creating new widget instances every rebuild
        Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.blue, Colors.purple],
            ),
          ),
          child: Text('Hello'),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The Fix:

class MyWidget extends StatelessWidget {
  // Define widgets as static final or const
  static final _gradient = LinearGradient(
    colors: [Colors.blue, Colors.purple],
  );

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          decoration: BoxDecoration(gradient: _gradient),
          child: const Text('Hello'),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Creating new widget instances on every rebuild is expensive. Use const constructors and extract complex widgets into separate classes or static variables.

2. Not Using const Constructors

The Mistake:

Column(
  children: [
    Text('Static text'),
    Icon(Icons.star),
    SizedBox(height: 20),
  ],
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

Column(
  children: [
    const Text('Static text'),
    const Icon(Icons.star),
    const SizedBox(height: 20),
  ],
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: The const keyword tells Flutter that a widget is immutable and can be reused. This prevents unnecessary widget rebuilds and saves memory.

3. Overusing setState() with Large Widget Trees

The Mistake:

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Large widget tree
          ExpensiveWidget(),
          AnotherExpensiveWidget(),
          Text('$counter'), // Only this needs to update
          YetAnotherExpensiveWidget(),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => counter++),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The Fix:

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // These won't rebuild
          const ExpensiveWidget(),
          const AnotherExpensiveWidget(),
          // Extract counter to separate widget
          CounterDisplay(counter: counter),
          const YetAnotherExpensiveWidget(),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => counter++),
      ),
    );
  }
}

class CounterDisplay extends StatelessWidget {
  final int counter;
  const CounterDisplay({Key? key, required this.counter}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Text('$counter');
  }
}
Enter fullscreen mode Exit fullscreen mode

Why It Matters: setState() rebuilds the entire widget tree. Extract frequently changing parts into separate widgets to minimize rebuilds.

4. Using Opacity for Hiding Widgets

The Mistake:

Opacity(
  opacity: isVisible ? 1.0 : 0.0,
  child: ExpensiveWidget(),
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

// Option 1: Use Visibility
Visibility(
  visible: isVisible,
  child: ExpensiveWidget(),
)

// Option 2: Conditional rendering
if (isVisible) ExpensiveWidget(),
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Opacity still renders the widget even when invisible, consuming resources. Use Visibility widget or conditional rendering instead.

5. Not Implementing ListView.builder for Large Lists

The Mistake:

ListView(
  children: List.generate(1000, (index) => ListTile(
    title: Text('Item $index'),
  )),
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

ListView.builder(
  itemCount: 1000,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text('Item $index'),
    );
  },
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: ListView.builder creates widgets on-demand as they become visible, dramatically improving performance for large lists.

6. Excessive Use of ClipRRect and ClipPath

The Mistake:

ClipRRect(
  borderRadius: BorderRadius.circular(10),
  child: Container(
    color: Colors.blue,
    child: Text('Clipped content'),
  ),
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

Container(
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(10),
  ),
  child: Text('Content'),
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Clipping operations are expensive. Use Container with BoxDecoration for simple rounded corners instead of ClipRRect.

7. Loading Large Images Without Optimization

The Mistake:

Image.asset('assets/large_image.jpg') // 4MB image
Enter fullscreen mode Exit fullscreen mode

The Fix:

Image.asset(
  'assets/large_image.jpg',
  width: 200,
  height: 200,
  fit: BoxFit.cover,
  cacheWidth: 400, // Resize image in memory
  cacheHeight: 400,
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Large images consume significant memory. Always specify dimensions and use cacheWidth/cacheHeight to resize images in memory.

8. Not Using RepaintBoundary for Expensive Widgets

The Mistake:

CustomPaint(
  painter: ComplexChartPainter(),
  child: Container(
    // Complex drawing operations
  ),
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

RepaintBoundary(
  child: CustomPaint(
    painter: ComplexChartPainter(),
    child: Container(
      // Complex drawing operations
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: RepaintBoundary isolates expensive painting operations, preventing them from affecting other widgets during repaints.

9. Synchronous Operations on the Main Thread

The Mistake:

void loadData() {
  // Blocking operation on main thread
  String data = heavyComputation();
  setState(() {
    // Update UI
  });
}
Enter fullscreen mode Exit fullscreen mode

The Fix:

Future<void> loadData() async {
  // Move heavy computation to isolate
  String data = await compute(heavyComputation, inputData);
  setState(() {
    // Update UI
  });
}

String heavyComputation(dynamic input) {
  // Heavy computation here
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Heavy computations on the main thread cause UI freezing. Use compute() function to run expensive operations in isolates.

10. Overusing AnimatedBuilder Instead of Specific Animated Widgets

The Mistake:

AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Transform.scale(
      scale: _controller.value,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  },
)
Enter fullscreen mode Exit fullscreen mode

The Fix:

ScaleTransition(
  scale: _controller,
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
)
Enter fullscreen mode Exit fullscreen mode

Why It Matters: Specific animated widgets like ScaleTransition, FadeTransition, and SlideTransition are more optimized than generic AnimatedBuilder.

Performance Monitoring Tips 🎩

To catch performance issues early:

  1. Use Flutter Inspector to identify widget rebuild issues
  2. Enable performance overlay with flutter run --profile
  3. Use Timeline in DevTools to analyze frame rendering times
  4. Monitor memory usage in DevTools Memory tab
  5. Test on real devices, especially lower-end ones

Conclusion 🀯

Performance optimization is an ongoing process. These 10 mistakes are common but easily fixable with the right approach. Remember to profile your app regularly and always test on real devices.

The key is to think about widget rebuilds, memory usage, and main thread blocking operations. By avoiding these common pitfalls, you'll create Flutter apps that feel smooth and responsive.

Have you encountered any of these performance issues in your Flutter projects? Share your experiences in the comments below!

Top comments (0)