DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 22: Performance Optimization Basics - Speeding Up at the Code Level

In previous lessons, we covered code conventions and static analysis, mastering the fundamentals of writing high-quality code. Today we'll focus on performance optimization — improving program efficiency at the code level. Performance is crucial for user experience, especially in mobile applications and high-performance scenarios, where even millisecond-level optimizations can significantly enhance the user experience.

I. Type Optimization: Avoid Unnecessary dynamic Types

Dart is a strongly typed language, but it also supports the dynamic type, which disables type checking and allows variables to store values of any type. However, overusing dynamic can significantly impact performance and code safety.

1. Performance Issues with dynamic

  • Runtime type checking overhead: When using dynamic, the Dart VM must constantly check the actual type of variables at runtime, whereas static types (like String, int) have their types determined at compile time with no runtime overhead.
  • Hinders compiler optimizations: Static types enable compilers to perform more optimizations (like inlining, type specialization), while dynamic invalidates these optimizations.
  • Hides potential errors: Compile-time type checking is disabled, potentially leading to runtime exceptions (such as "A value of type 'num' can't be assigned to a variable of type 'int'").

2. Replacing dynamic with Specific Types

// Not recommended: Using dynamic
void processData(dynamic data) {
  print(data.length); // No compile-time check if data has length property
}

// Recommended: Using specific type
void processList(List<String> data) {
  print(data.length); // Type is明确 at compile time, safe and efficient
}

// Recommended: Using generics (balances flexibility and type safety)
void processGeneric<T>(List<T> data) {
  print(data.length);
}
Enter fullscreen mode Exit fullscreen mode

3. Performance Test Comparison and Analysis

Install the benchmark_harness library
When testing the performance of dynamic vs. static types, results can vary by scenario:

import 'dart:math';
import 'package:benchmark_harness/benchmark_harness.dart';

// Dynamic type version
class DynamicBenchmark extends BenchmarkBase {
  const DynamicBenchmark() : super('Dynamic type');

  @override
  void run() {
    dynamic list = [1, 2, 3, 4, 5];
    int sum = 0;
    for (int i = 0; i < 1000; i++) {
      for (var item in list) {
        sum += item as int;
      }
    }
  }
}

// Static type version
class StaticBenchmark extends BenchmarkBase {
  const StaticBenchmark() : super('Static type');

  @override
  void run() {
    List<int> list = [1, 2, 3, 4, 5];
    int sum = 0;
    for (int i = 0; i < 1000; i++) {
      for (var item in list) {
        sum += item;
      }
    }
  }
}

void main() {
  const DynamicBenchmark().report();
  const StaticBenchmark().report();
}
Enter fullscreen mode Exit fullscreen mode

Test results (for reference only):

Dynamic type(RunTime): 116.65289702467736 us.
Static type(RunTime): 107.3150159744409 us.
Enter fullscreen mode Exit fullscreen mode
Interpreting Results:

In simple scenarios, dynamic types might perform similarly to static types due to the VM's "type specialization optimization" (remembering the actual type). However, in complex scenarios (involving type conversions, conditional checks), static types show their advantages by avoiding runtime checks and conversion overhead.

Best Practice: Unless working with dynamic data sources like JSON parsing, always use specific types or generics to ensure type safety and reduce runtime overhead.


II. Collection Operation Optimization: Performance Comparison of Loops and Iterations

Collections (List, Set, Map) are the most commonly used data structures in programming, and the performance of their iteration operations directly impacts program efficiency. Performance differences between iteration methods aren't absolute but depend on list type, loop body complexity, and type operations.

1. Performance Comparison of Three Common Loops

Let's analyze the performance differences between for loops, forEach, and for-in loops through practical testing:

import 'package:benchmark_harness/benchmark_harness.dart';

// Test dynamic list (simulating complex scenarios)
final list = List.generate(10000, (i) => i).cast<dynamic>();

// for loop (index access)
class ForLoopBenchmark extends BenchmarkBase {
  const ForLoopBenchmark() : super('For loop');

  @override
  void run() {
    int sum = 0;
    for (int i = 0; i < list.length; i++) {
      if (list[i] % 2 == 0) {
        sum +=
            (list[i] * 2)
                as int; // Contains conditional check and type conversion
      }
    }
  }
}

// forEach iteration (function callback)
class ForEachBenchmark extends BenchmarkBase {
  const ForEachBenchmark() : super('forEach');

  @override
  void run() {
    int sum = 0;
    list.forEach((item) {
      if (item % 2 == 0) {
        sum += item * 2 as int;
      }
    });
  }
}

// for-in loop (iterator)
class ForInBenchmark extends BenchmarkBase {
  const ForInBenchmark() : super('for-in loop');

  @override
  void run() {
    int sum = 0;
    for (final item in list) {
      if (item % 2 == 0) {
        sum += item * 2 as int;
      }
    }
  }
}

void main() {
  const ForLoopBenchmark().report();
  const ForEachBenchmark().report();
  const ForInBenchmark().report();
}
Enter fullscreen mode Exit fullscreen mode

Test Results:

For loop(RunTime): 483.3015179392824 us.
forEach(RunTime): 594.60975 us.
for-in loop(RunTime): 460.9502030348365 us.
Enter fullscreen mode Exit fullscreen mode

2. Analysis: Why did for-in perform best?

In this dynamic list + type conversion scenario, performance differences stem from their underlying implementation mechanisms:

Loop Type Performance Core Reason Analysis
for-in Best Uses iterator pattern with special optimizations for dynamic lists: 1. Acquires traversal rights once, reducing repeated boundary checks 2. Caches type information, reducing dynamic conversion overhead 3. Iterator implementation is deeply optimized by compiler (loop unrolling, etc.)
for Second Index access has additional overhead: 1. Each list[i] requires independent boundary checks 2. Index access on dynamic lists triggers more type validation 3. Loop condition i < list.length may recalculate length
forEach Slowest Function callback introduces overhead: 1. Fixed cost of anonymous function calls (parameter passing, stack operations) 2. More frequent type checks for function parameters with dynamic types 3. Complex loop bodies amplify function call overhead

3. How Scenarios Impact Performance

Loop performance isn't constant but highly scenario-dependent:

  • List Type:
    • For static type lists (List), for loop index access may be optimal (compiler can remove redundant checks).
    • For dynamic type lists (List), for-in iterator optimizations are more significant.
  • Loop Body Complexity:
    • With simple operations (like pure addition), iteration method differences have more impact.
    • With complex operations (like heavy calculations, network requests), performance gaps between loops diminish (loop body overhead dominates).
  • Need for Index:
    • When an index is needed, for loop is the only choice.
    • When no index is needed, for-in or forEach is cleaner (choose based on performance needs).

Best Practice: Choose loop type based on scenario — prefer for-in for dynamic lists with complex type operations; use for loops for static lists with simple operations; use forEach for code clarity when performance isn't critical.


III. Object Creation Optimization: const Constructors and Object Reuse

Object creation is a major source of memory allocation. Frequent creation of short-lived objects increases garbage collection (GC) pressure and causes performance fluctuations. Using const constructors and object reuse can effectively reduce object creation.

1. const Constructors: Compile-time Immutable Objects

Objects created with const constructors are compile-time constants. Identical const objects exist only once in memory and don't trigger runtime memory allocation.

// Regular constructor
class Point {
  final int x;
  final int y;

  Point(this.x, this.y);
}

// Constant constructor
class ConstPoint {
  final int x;
  final int y;

  const ConstPoint(this.x, this.y); // Note the const keyword
}

void main() {
  // Regular objects: New instance created each time
  final p1 = Point(1, 2);
  final p2 = Point(1, 2);
  print(identical(p1, p2)); // Output: false (different instances)

  // Constant objects: Identical parameters create identical instances
  final cp1 = const ConstPoint(1, 2);
  final cp2 = const ConstPoint(1, 2);
  print(identical(cp1, cp2)); // Output: true (same instance)
}
Enter fullscreen mode Exit fullscreen mode

2. Suitable Scenarios for const Constructors

  • Storing immutable data (configuration items, constant definitions).
  • Building static Widgets in Flutter (text, icons) to avoid recreating objects during rebuilds.
// Flutter example: Using const to reduce Widget rebuilds
Widget build(BuildContext context) {
  return Column(
    children: [
      // Static text: Using const avoids new object on each build
      const Text("Static Title"),

      // Dynamic content: Cannot use const
      Text("Username: ${user.name}"),
    ],
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Object Reuse: Caching and Pooling

For frequently created and destroyed objects (like temporary calculation objects), you can reuse instances through caching or object pooling:

// Caching example (Singleton pattern)
class DataParser {
  // Cached parser instance
  static final DataParser _instance = DataParser._();

  // Private constructor to prevent external creation
  DataParser._();

  // Factory method to get singleton
  factory DataParser() => _instance;

  // Parsing logic
  Map<String, dynamic> parse(String data) {
    // Parsing implementation...
  }
}

// Object pool example (for frequently created lightweight objects)
class ObjectPool<T> {
  final List<T> _pool = [];
  final T Function() _creator;

  ObjectPool(this._creator);

  // Get object (from pool if available, create new otherwise)
  T acquire() {
    if (_pool.isNotEmpty) {
      return _pool.removeLast();
    }
    return _creator();
  }

  // Release object (return to pool)
  void release(T obj) {
    _pool.add(obj);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Performance Test: const vs Regular Objects

import 'package:benchmark_harness/benchmark_harness.dart';

class NormalObject {
  final int value;
  NormalObject(this.value);
}

class ConstObject {
  final int value;
  const ConstObject(this.value);
}

class NormalBenchmark extends BenchmarkBase {
  const NormalBenchmark() : super('Normal object');

  @override
  void run() {
    for (int i = 0; i < 10000; i++) {
      NormalObject(i); // Creates new object each time
    }
  }
}

class ConstBenchmark extends BenchmarkBase {
  const ConstBenchmark() : super('Const object');

  @override
  void run() {
    for (int i = 0; i < 10000; i++) {
      const ConstObject(1); // Reuses same instance
    }
  }
}

void main() {
  const ConstBenchmark().report();
  const NormalBenchmark().report();
}
Enter fullscreen mode Exit fullscreen mode

Example Test Results:

Const object(RunTime): 27.686003598987405 us.
Normal object(RunTime): 43.3430996202768 us.
Enter fullscreen mode Exit fullscreen mode

const object creation has almost no overhead, being approximately 36% faster than regular objects, with significant advantages especially when frequently creating identical objects.


IV. Other Performance Optimization Techniques

1. Avoid Unnecessary Recreations: final Keyword

Using final to declare variables not only ensures immutability but also helps compilers perform optimizations (like determining variables won't change, reducing checks):

// Recommended: Use final for variables that won't change
void process() {
  final list = [1, 2, 3]; // Reference is immutable (contents can change)
  final sum = list.fold(0, (a, b) => a + b); // Immutable variable
}
Enter fullscreen mode Exit fullscreen mode

2. Choose Efficient Data Structures

  • Containment checks: Set's contains operation is O(1), while List's contains is O(n). Prefer Set for large datasets.
  • Key-value lookups: Map lookup efficiency is much higher than linear search. Use Map for frequent key-based queries.
// Inefficient: List contains check (O(n))
bool isInList(List<int> list, int value) => list.contains(value);

// Efficient: Set contains check (O(1))
bool isInSet(Set<int> set, int value) => set.contains(value);
Enter fullscreen mode Exit fullscreen mode

3. Lazy Loading: Deferred Initialization

Use the late keyword or Future to delay initialization of time-consuming objects, avoiding startup performance overhead:

class HeavyService {
  // Lazy initialization (created only on first access)
  late final Database _db = _initDatabase();

  Database _initDatabase() {
    // Simulate time-consuming initialization
    return Database();
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Avoid Time-Consuming Operations in High-Frequency Functions

In Flutter's build method or frequently called functions, avoid time-consuming operations like calculations or network requests that can cause UI jank:

// Not recommended: Performing calculations in build
Widget build(BuildContext context) {
  final data = _calculateComplexData(); // Time-consuming calculation
  return Text(data.toString());
}

// Recommended: Calculate and cache results in advance
Widget build(BuildContext context) {
  return FutureBuilder(
    future: _cachedData, // Pre-calculated and cached
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        return Text(snapshot.data.toString());
      }
      return CircularProgressIndicator();
    },
  );
}
Enter fullscreen mode Exit fullscreen mode

V. Performance Analysis Tools: Finding Optimization Points

Before optimizing, you need to identify performance bottlenecks. Dart provides several tools to help analyze performance:

1. dart devtools

A set of Dart development tools including a Performance analyzer, Memory analyzer, and more:

# Launch DevTools
dart devtools
Enter fullscreen mode Exit fullscreen mode

Open the tool in a browser to view function execution times, memory allocation, and other real-time information.

2. benchmark_harness Package

Used for writing microbenchmarks to accurately measure execution time of code snippets (like our performance test examples).

3. Flutter Performance Panel

In Flutter development, use DevTools' Performance panel to monitor UI rendering frame rates, build times, and help locate jank issues.

Top comments (0)