DEV Community

Cover image for Optimizing Performance in Flutter App: Best Practices
Harsh Bangari Rawat
Harsh Bangari Rawat

Posted on • Edited on

Optimizing Performance in Flutter App: Best Practices

Performance is the bedrock of a successful mobile app. By prioritizing speed, responsiveness, and a smooth user experience, developers can create apps that users will love to use, keep coming back to, and recommend to others. Just like a smooth-running car is more enjoyable to drive than a sluggish one, a well-performing app is far more engaging and satisfying to use than its laggy counterpart.

Performance Benefits Everyone:

  • Improved Battery Life: Efficiently coded apps consume less battery, allowing users to enjoy their devices for longer.
  • Reduced Development Costs: Optimizing performance early in development can save time and resources compared to fixing issues later.

Key Strategies for Performance Optimization:

1. Minimize Widget Rebuilds:

The build method is the heart of every widget in Flutter. It's responsible for defining the widget's visual representation and layout. Flutter uses a reactive approach where the UI is rebuilt whenever the widget or its dependencies change. While this is powerful for keeping the UI up-to-date, unnecessary rebuilds can significantly impact performance.

The Problem with Unnecessary Rebuilds:

  • Rebuilding is Expensive: It involves recreating the widget tree and potentially re-rendering the UI. This process takes time and resources, especially for complex widgets.
  • Frequent Rebuilds = Laggy UI: If a widget rebuilds too often, it can lead to a stuttering or unresponsive feeling in the app. Users might perceive this as slow performance.

Common scenarios for unnecessary rebuilds:

  • Complex Logic in build Method: Performing expensive calculations or data fetching within the build method can trigger rebuilds even if the data hasn't changed.
class CarDashboard extends StatefulWidget {
  @override
  _CarDashboardState createState() => _CarDashboardState();
}

class _CarDashboardState extends State<CarDashboard> {
  int _fuelLevel = 75;
  int _odometerReading = 12000;
  double _averageFuelEfficiency = 0.0; // Initially empty

  @override
  void initState() {
    super.initState();
    // Fetch initial fuel efficiency data from API (not shown for brevity)
  }

  @override
  Widget build(BuildContext context) {
    _averageFuelEfficiency = calculateFuelEfficiency(_fuelLevel, _odometerReading); // Calculate every time build is called

    return Scaffold(
      // ...dashboard widgets...
      Text('Average Fuel Efficiency: ${_averageFuelEfficiency} L/100km'),
    );
  }

  double calculateFuelEfficiency(int fuelLevel, int odometerReading) {
    // complex calculation for distance traveled
    return // ... (complex calculation logic);
  }
}
Enter fullscreen mode Exit fullscreen mode

The calculateFuelEfficiency is called every time the build method is triggered, even if the fuel level and odometer haven't changed. This can be unnecessary and lead to performance issues.

Use the didUpdateWidget Lifecycle Method:

@override
void didUpdateWidget(CarDashboard oldWidget) {
  super.didUpdateWidget(oldWidget);

  if (_fuelLevel != oldWidget._fuelLevel || _odometerReading != oldWidget._odometerReading) {
    _averageFuelEfficiency = calculateFuelEfficiency(_fuelLevel, _odometerReading);
    setState(() {}); // Trigger rebuild only when data changes
  }
}
Enter fullscreen mode Exit fullscreen mode

The didUpdateWidget lifecycle method is called whenever the widget receives new data. This is a good place to update cached values based on changes and trigger a rebuild if necessary.

  • Mutable Data in build Method: Using mutable data (data that can be changed) inside the build method can lead to rebuilds whenever the data is modified.
  • Unnecessary setState Calls: In stateful widgets, calling setState even for minor changes triggers a rebuild, even if the UI doesn't visually change.

Optimizing for Fewer Rebuilds:

Here's how you can minimize unnecessary rebuilds and improve your app's performance:

  • Use Stateless Widgets When Possible: Stateless widgets are simpler and only rebuild when their input data changes. This makes them ideal for static UI elements.
  • Optimize build Method Logic: Keep the logic within the build method as simple and efficient as possible. Consider moving complex calculations or data fetching outside the build method.
  • Use const for Unchanging Values: The const keyword helps Flutter understand that a value is constant and won't change. This prevents unnecessary rebuilds when the value is used within the build method.
  • Memoization Techniques: For widgets that rely on expensive calculations, consider using memoization techniques to cache the results and avoid redundant calculations.
  • Efficient State Management: Choose the appropriate state management solution (e.g., Provider, Bloc) and avoid unnecessary state updates.

2. Optimize Widget Trees:

Every app's UI is built using a hierarchy of widgets. This hierarchy is called a widget tree. Each widget represents a visual element on the screen, and it can contain other widgets as its children, forming a tree-like structure.

Benefits:

  • Modular Design: Widgets promote modularity, allowing you to break down complex UIs into smaller, reusable components. This simplifies development and maintenance.
  • Flexibility: The hierarchical structure allows for flexible layouts and nesting of widgets to create a wide variety of UI elements.

While widget trees offer advantages, their complexity can negatively impact rendering performance. Here's why:

  • Rendering Cost: When the UI needs to be updated (due to data changes, user interaction, etc.), Flutter traverses the entire widget tree.
  • Complex Trees Take Time: The more complex the widget tree (with many nested widgets), the longer it takes for Flutter to rebuild and render the UI. This can lead to lag or dropped frames, affecting smoothness.

Here's how complex widget trees can slow down rendering:

  • Increased Depth: Deeper nesting of widgets means more levels to traverse during the rebuild process.
  • Large Number of Widgets: A large number of widgets in the tree can significantly increase the amount of work required for rendering.
  • Expensive Widgets: If individual widgets are complex or inefficient, it can take longer to render them, impacting overall performance.

Optimizing Widget Trees for Performance:

Here are some strategies to keep your widget trees lean and efficient:

  • Favor Shallow Structures: Aim for flatter widget trees with fewer levels of nesting. This reduces the traversal time during rebuilds.

Deeply Nested Tree (Less Optimized)

class CarDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // Speedometer (Nested deeply)
        Column(
          children: [
            Text('Speedometer'),
            Text('100 MPH'), // Speed value
          ],
        ),
        // ...other deeply nested widgets for buttons and labels...
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here the Speedometer widget is nested within a Column within a Stack. This creates a deeper nesting level, potentially impacting performance.

Flatter Widget Tree (Recommended)

class CarDashboard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        SpeedometerWidget(speed: 100), // Pass speed value as a property
        // ...other widgets similarly flattened, avoiding deep nesting...
      ],
    );
  }
}

class SpeedometerWidget extends StatelessWidget {
  final int speed;

  const SpeedometerWidget({Key? key, required this.speed}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Speedometer'),
        Text('$speed MPH'), // Use speed property from parent
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the SpeedometerWidget is separated from the CarDashboard. The speed value is passed as a property, reducing nesting and improving performance.

  • Break Down Complex Widgets: If a widget becomes too complex, consider dividing it into smaller, more manageable sub-widgets with simpler responsibilities.
  • Use Stateless Widgets When Possible: Stateless widgets are generally faster to render compared to stateful widgets. Use them for UI elements that don't require dynamic updates.
  • Leverage Caching Mechanisms: Consider using caching techniques for frequently used widget trees or sub-trees to avoid redundant creation and rendering.
class CarData extends InheritedWidget {
  final int fuelLevel;
  final int odometerReading;

  const CarData({
    Key? key,
    required this.fuelLevel,
    required this.odometerReading,
    required Widget child,
  }) : super(key: key, child: child);

  static CarData of(BuildContext context) {
    final CarData? data = context.dependOnInheritedWidgetOfExactType<CarData>();
    if (data != null) return data; else throw Exception('CarData not found!');
  }

  @override
  bool updateShouldNotify(CarData oldWidget) =>
      fuelLevel != oldWidget.fuelLevel || odometerReading != oldWidget.odometerReading;
}

class FuelGaugeAndOdometer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final carData = CarData.of(context);

    // Access cached data for fuel level and odometer reading
    return Row(
      children: [
        Text('Fuel: ${carData.fuelLevel} %'),
        Spacer(),
        Text('Odometer: ${carData.odometerReading} km'),
      ],
    );
  }
}

class CarDashboard extends StatefulWidget {
  @override
  _CarDashboardState createState() => _CarDashboardState();
}

class _CarDashboardState extends State<CarDashboard> {
  int _fuelLevel = 75;
  int _odometerReading = 12000;

  void updateCarData() {
    setState(() {
      _fuelLevel = _fuelLevel - 5; // Simulate fuel consumption
      _odometerReading += 10; // Simulate driving distance
    });
  }

  @override
  Widget build(BuildContext context) {
    return CarData( // Wrap the widget tree with CarData
      fuelLevel: _fuelLevel,
      odometerReading: _odometerReading,
      child: Scaffold(
        // ...other dashboard widgets...
        floatingActionButton: FloatingActionButton(
          onPressed: updateCarData,
          child: Icon(Icons.refresh),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The FuelGaugeAndOdometer widget reuses the cached data from CarData instead of rebuilding the entire sub-tree whenever the fuel level or odometer reading changes.
This reduces unnecessary widget creation and rendering, improving performance, especially when the fuel gauge and odometer are frequently displayed on the screen.

A well-optimized widget tree is not just about the number of widgets, but also about their complexity and efficiency.

Implementing these techniques will make a significant difference in your Flutter apps. 🚀
Thanks for reading!🙏🏻 and I hope you found this blog informative. Feel free to share it with others who might find it helpful.🏆

Keep Coding... 🧑🏻‍💻

Top comments (0)