ADVANCEDperformanceā± 30 min read

Performance Optimization for Reactive Applications

Advanced techniques for optimizing reactive stream performance, reducing rebuilds, and improving app efficiency

Reactiv TeamUpdated 1/15/2025

šŸ“š Prerequisites

  • Strong understanding of reactive programming
  • Experience with Reactiv Core and Flutter
  • Familiarity with profiling tools
#performance#optimization#streams#memory#profiling#best-practices

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

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: true to prevent duplicate emissions
  • Combine multiple streams instead of multiple builders
  • Create streams in initState(), not build()
  • Use const constructors 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:

  1. Smart stream usage: Distinct values, combined streams, lazy creation
  2. Selective rebuilding: Only rebuild what changes
  3. Memory management: Proper disposal and cleanup
  4. Efficient updates: Batching and conditional updates
  5. Debouncing/throttling: Control update frequency
  6. Profiling: Regular performance monitoring
  7. 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.