Performance Optimization for Reactive Applications
Advanced techniques for optimizing reactive stream performance, reducing rebuilds, and improving app efficiency
š Prerequisites
- Strong understanding of reactive programming
- Experience with Reactiv Core and Flutter
- Familiarity with profiling tools
Performance Optimization for Reactive Applications
Reactive programming offers powerful capabilities, but improper use can lead to performance issues. This guide covers advanced optimization techniques for building high-performance reactive applications.
Table of Contents
- Understanding Performance Bottlenecks
- Stream Optimization
- Reducing Unnecessary Rebuilds
- Memory Management
- Efficient State Updates
- Debouncing and Throttling
- Profiling and Debugging
- Advanced Patterns
- Best Practices
Understanding Performance Bottlenecks
Common performance issues in reactive applications:
- Excessive rebuilds: Widgets rebuild unnecessarily
- Memory leaks: Subscriptions not properly disposed
- Stream overhead: Too many active streams
- Computation waste: Heavy operations on every emission
- Cascading updates: One change triggers multiple updates
Performance Goals:
- Keep frame rate at 60 FPS (16ms per frame)
- Minimize memory allocations
- Reduce subscription overhead
- Optimize state update frequency
Stream Optimization
Use Distinct to Prevent Duplicate Emissions
// ā Bad: Emits duplicate values
final counter = ReactiveValue<int>(0);
counter.value = 1;
counter.value = 1; // Triggers listeners again
counter.value = 1; // And again!
// ā
Good: Only emits when value changes
final counter = ReactiveValue<int>(
0,
distinct: true, // Built-in distinctness
);
counter.value = 1;
counter.value = 1; // Ignored
counter.value = 1; // Ignored
Combine Multiple Streams Efficiently
// ā Bad: Multiple subscriptions, multiple rebuilds
class UserProfile extends StatelessWidget {
final ReactiveValue<String> firstName;
final ReactiveValue<String> lastName;
final ReactiveValue<int> age;
Widget build(BuildContext context) {
return Column(
children: [
ReactiveBuilder(
reactive: firstName,
builder: (context, value) => Text(value),
),
ReactiveBuilder(
reactive: lastName,
builder: (context, value) => Text(value),
),
ReactiveBuilder(
reactive: age,
builder: (context, value) => Text('$value'),
),
],
);
}
}
// ā
Good: Single combined stream, single rebuild
class UserProfile extends StatelessWidget {
final ReactiveValue<String> firstName;
final ReactiveValue<String> lastName;
final ReactiveValue<int> age;
late final combined = Rx.combineLatest3(
firstName.stream,
lastName.stream,
age.stream,
(first, last, age) => {
'fullName': '$first $last',
'age': age,
},
);
Widget build(BuildContext context) {
return StreamBuilder(
stream: combined,
builder: (context, snapshot) {
if (!snapshot.hasData) return SizedBox.shrink();
final data = snapshot.data!;
return Column(
children: [
Text(data['fullName']),
Text('Age: ${data['age']}'),
],
);
},
);
}
}
Lazy Stream Creation
// ā Bad: Creates stream immediately
class ExpensiveData {
final Stream<List<int>> dataStream = _computeExpensiveData();
static Stream<List<int>> _computeExpensiveData() {
// Heavy computation runs immediately
return Stream.periodic(
Duration(seconds: 1),
(_) => List.generate(10000, (i) => i * i),
);
}
}
// ā
Good: Creates stream only when needed
class ExpensiveData {
Stream<List<int>>? _dataStream;
Stream<List<int>> get dataStream {
return _dataStream ??= _computeExpensiveData();
}
Stream<List<int>> _computeExpensiveData() {
return Stream.periodic(
Duration(seconds: 1),
(_) => List.generate(10000, (i) => i * i),
);
}
}
Reducing Unnecessary Rebuilds
Selective Rebuilding
// ā Bad: Entire widget rebuilds on any change
class Dashboard extends StatelessWidget {
final ReactiveValue<int> counter;
final ReactiveValue<String> title;
final ReactiveValue<bool> isLoading;
Widget build(BuildContext context) {
return ReactiveBuilder(
reactive: Rx.combineLatest3(
counter.stream,
title.stream,
isLoading.stream,
(a, b, c) => null,
),
builder: (context, _) {
return Column(
children: [
Text(title.value), // Rebuilds even when counter changes
Text('${counter.value}'), // Rebuilds even when title changes
if (isLoading.value) CircularProgressIndicator(),
ExpensiveWidget(), // Always rebuilds!
],
);
},
);
}
}
// ā
Good: Only rebuild what changes
class Dashboard extends StatelessWidget {
final ReactiveValue<int> counter;
final ReactiveValue<String> title;
final ReactiveValue<bool> isLoading;
Widget build(BuildContext context) {
return Column(
children: [
ReactiveBuilder(
reactive: title,
builder: (context, value) => Text(value),
),
ReactiveBuilder(
reactive: counter,
builder: (context, value) => Text('$value'),
),
ReactiveBuilder(
reactive: isLoading,
builder: (context, isLoading) => isLoading
? CircularProgressIndicator()
: SizedBox.shrink(),
),
const ExpensiveWidget(), // Never rebuilds!
],
);
}
}
Use const Constructors
// ā Bad: Widget recreated on every build
return Container(
padding: EdgeInsets.all(16),
child: Text('Static Text'),
);
// ā
Good: Widget reused from cache
return Container(
padding: const EdgeInsets.all(16),
child: const Text('Static Text'),
);
Memory Management
Proper Disposal
// ā Bad: Memory leak - never disposed
class MyController {
final data = ReactiveValue<String>('');
// No dispose method!
}
// ā
Good: Proper cleanup
class MyController {
final data = ReactiveValue<String>('');
final _subscriptions = <StreamSubscription>[];
void initialize() {
_subscriptions.add(
someStream.listen((value) {
data.value = value;
}),
);
}
void dispose() {
data.dispose();
for (final sub in _subscriptions) {
sub.cancel();
}
_subscriptions.clear();
}
}
CompositeSubscription Pattern
class CompositeSubscription {
final _subscriptions = <StreamSubscription>[];
void add(StreamSubscription subscription) {
_subscriptions.add(subscription);
}
Future<void> dispose() async {
await Future.wait(_subscriptions.map((s) => s.cancel()));
_subscriptions.clear();
}
}
// Usage
class ViewController {
final _subscriptions = CompositeSubscription();
void initialize() {
_subscriptions.add(stream1.listen(...));
_subscriptions.add(stream2.listen(...));
_subscriptions.add(stream3.listen(...));
}
Future<void> dispose() async {
await _subscriptions.dispose();
}
}
Avoid Creating New Streams in Build Methods
// ā Bad: New stream created on every build
Widget build(BuildContext context) {
return StreamBuilder(
stream: repository.getUserStream(userId), // New stream every build!
builder: (context, snapshot) => Text('${snapshot.data}'),
);
}
// ā
Good: Stream created once
class UserWidget extends StatefulWidget {
final String userId;
const UserWidget({required this.userId});
State<UserWidget> createState() => _UserWidgetState();
}
class _UserWidgetState extends State<UserWidget> {
late final Stream<User> userStream;
void initState() {
super.initState();
userStream = repository.getUserStream(widget.userId);
}
Widget build(BuildContext context) {
return StreamBuilder(
stream: userStream,
builder: (context, snapshot) => Text('${snapshot.data}'),
);
}
}
Efficient State Updates
Batch Updates
// ā Bad: Multiple emissions
void updateUserInfo(String first, String last, int age) {
firstName.value = first; // Emission 1
lastName.value = last; // Emission 2
userAge.value = age; // Emission 3
// 3 separate rebuilds!
}
// ā
Good: Single update
class UserInfo {
final String firstName;
final String lastName;
final int age;
UserInfo(this.firstName, this.lastName, this.age);
bool operator ==(Object other) =>
identical(this, other) ||
other is UserInfo &&
firstName == other.firstName &&
lastName == other.lastName &&
age == other.age;
int get hashCode => Object.hash(firstName, lastName, age);
}
final userInfo = ReactiveValue<UserInfo>(
UserInfo('', '', 0),
distinct: true,
);
void updateUserInfo(String first, String last, int age) {
userInfo.value = UserInfo(first, last, age); // Single emission!
}
Conditional Updates
// ā Bad: Always updates even if value unchanged
void updateCounter(int newValue) {
counter.value = newValue;
}
// ā
Good: Only update if actually changed
void updateCounter(int newValue) {
if (counter.value != newValue) {
counter.value = newValue;
}
}
// ā
Better: Use distinct flag
final counter = ReactiveValue<int>(0, distinct: true);
// Now automatically skips duplicate values
Debouncing and Throttling
Debounce User Input
// Search as user types - debounced
class SearchController {
final searchQuery = ReactiveValue<String>('');
final searchResults = ReactiveValue<List<String>>([]);
late final StreamSubscription _searchSubscription;
SearchController() {
_searchSubscription = searchQuery.stream
.debounceTime(Duration(milliseconds: 300)) // Wait 300ms after last input
.distinct() // Ignore duplicate queries
.switchMap((query) => _performSearch(query)) // Cancel previous search
.listen((results) {
searchResults.value = results;
});
}
Stream<List<String>> _performSearch(String query) async* {
if (query.isEmpty) {
yield [];
return;
}
final results = await api.search(query);
yield results;
}
void dispose() {
_searchSubscription.cancel();
searchQuery.dispose();
searchResults.dispose();
}
}
Throttle Scroll Events
class ScrollController {
final scrollPosition = ReactiveValue<double>(0);
late final StreamSubscription _scrollSubscription;
ScrollController() {
_scrollSubscription = scrollPosition.stream
.throttleTime(Duration(milliseconds: 100)) // Max 10 updates/sec
.listen(_handleScroll);
}
void _handleScroll(double position) {
// Update UI based on scroll position
print('Scroll: $position');
}
void dispose() {
_scrollSubscription.cancel();
scrollPosition.dispose();
}
}
Profiling and Debugging
Performance Monitoring
class PerformanceMonitor {
static void measureStreamPerformance<T>(
Stream<T> stream,
String label,
) {
final stopwatch = Stopwatch();
var emissionCount = 0;
stream.listen(
(value) {
emissionCount++;
final elapsed = stopwatch.elapsedMilliseconds;
print('[$label] Emission #$emissionCount at ${elapsed}ms');
},
onDone: () {
stopwatch.stop();
print('[$label] Total: $emissionCount emissions in ${stopwatch.elapsedMilliseconds}ms');
},
);
stopwatch.start();
}
}
// Usage
PerformanceMonitor.measureStreamPerformance(
myReactiveValue.stream,
'Counter Updates',
);
Memory Leak Detection
class MemoryLeakDetector {
static final _activeSubs = <String, int>{};
static StreamSubscription<T> track<T>(
StreamSubscription<T> subscription,
String label,
) {
_activeSubs[label] = (_activeSubs[label] ?? 0) + 1;
print('ā [$label] Active: ${_activeSubs[label]}');
subscription.onDone(() {
_activeSubs[label] = (_activeSubs[label] ?? 1) - 1;
print('ā [$label] Active: ${_activeSubs[label]}');
});
return subscription;
}
static void report() {
print('\nš Subscription Report:');
_activeSubs.forEach((label, count) {
final status = count > 0 ? 'ā ļø LEAK' : 'ā
OK';
print('$status [$label]: $count active');
});
}
}
Advanced Patterns
Computed Values with Caching
class ExpensiveComputation {
final ReactiveValue<int> input = ReactiveValue<int>(0);
late final ReactiveValue<int> output;
ExpensiveComputation() {
output = ReactiveValue<int>(
_compute(input.value),
distinct: true,
);
input.stream
.debounceTime(Duration(milliseconds: 100))
.map(_compute)
.listen((value) => output.value = value);
}
int _compute(int value) {
// Expensive operation
var result = 0;
for (int i = 0; i < value; i++) {
result += i * i;
}
return result;
}
void dispose() {
input.dispose();
output.dispose();
}
}
Resource Pooling
class StreamPool<T> {
final _pool = <Stream<T>>[];
final _maxSize = 10;
Stream<T> getOrCreate(Stream<T> Function() factory) {
if (_pool.isNotEmpty) {
return _pool.removeLast();
}
return factory();
}
void release(Stream<T> stream) {
if (_pool.length < _maxSize) {
_pool.add(stream);
}
}
}
Best Practices
ā Performance Optimization Checklist
- Use
distinct: trueto prevent duplicate emissions - Combine multiple streams instead of multiple builders
- Create streams in
initState(), notbuild() - Use
constconstructors wherever possible - Implement proper disposal for all reactive values
- Debounce user input (300-500ms)
- Throttle high-frequency events (scroll, mouse move)
- Batch state updates when possible
- Profile with DevTools regularly
- Monitor memory usage in production
Performance Metrics
Target Performance:
- Frame rate: 60 FPS (16ms per frame)
- Memory growth: < 1MB per minute idle
- Subscription count: Proportional to active features
- Rebuild frequency: Only on actual state changes
Common Anti-Patterns
ā Creating streams in build() ā Not disposing subscriptions ā Rebuilding entire widget trees ā No debouncing on user input ā Excessive stream nesting ā Ignoring distinct values
Summary
Optimizing reactive applications requires:
- Smart stream usage: Distinct values, combined streams, lazy creation
- Selective rebuilding: Only rebuild what changes
- Memory management: Proper disposal and cleanup
- Efficient updates: Batching and conditional updates
- Debouncing/throttling: Control update frequency
- Profiling: Regular performance monitoring
- Best practices: Follow established patterns
With these techniques, you can build high-performance reactive applications that remain responsive and efficient even with complex state management requirements.