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);
}
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();
}
Test results (for reference only):
Dynamic type(RunTime): 116.65289702467736 us.
Static type(RunTime): 107.3150159744409 us.
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();
}
Test Results:
For loop(RunTime): 483.3015179392824 us.
forEach(RunTime): 594.60975 us.
for-in loop(RunTime): 460.9502030348365 us.
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)
}
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}"),
],
);
}
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);
}
}
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();
}
Example Test Results:
Const object(RunTime): 27.686003598987405 us.
Normal object(RunTime): 43.3430996202768 us.
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
}
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);
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();
}
}
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();
},
);
}
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
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)