Performance Optimization
Optimize your InstantDB Flutter applications for maximum performance with efficient queries, smart caching strategies, and optimized UI patterns.
Query Optimization
Efficient Query Patterns
Write queries that minimize data transfer and processing:
// ✅ Good: Specific queries with filtersfinal recentPosts = db.subscribeQuery({ 'posts': { 'where': { 'published': true, 'createdAt': {'\$gte': DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch} }, 'orderBy': {'createdAt': 'desc'}, 'limit': 20, }});
// ❌ Avoid: Broad queries without filtersfinal allPosts = db.subscribeQuery({'posts': {}});
Pagination for Large Datasets
Implement efficient pagination to handle large amounts of data:
class PaginatedList extends StatefulWidget { final String entityType; final Map<String, dynamic>? whereClause; final int pageSize;
const PaginatedList({ super.key, required this.entityType, this.whereClause, this.pageSize = 20, });
@override State<PaginatedList> createState() => _PaginatedListState();}
class _PaginatedListState extends State<PaginatedList> { int _currentPage = 0; final List<Map<String, dynamic>> _allItems = []; bool _isLoading = false; bool _hasMore = true;
@override void initState() { super.initState(); _loadPage(0); }
Future<void> _loadPage(int page) async { if (_isLoading || !_hasMore) return;
setState(() { _isLoading = true; });
try { final db = InstantProvider.of(context); final result = await db.queryOnce({ widget.entityType: { if (widget.whereClause != null) 'where': widget.whereClause, 'orderBy': {'createdAt': 'desc'}, 'limit': widget.pageSize, 'offset': page * widget.pageSize, } });
final newItems = (result.data?[widget.entityType] as List? ?? []) .cast<Map<String, dynamic>>();
setState(() { if (page == 0) { _allItems.clear(); } _allItems.addAll(newItems); _hasMore = newItems.length == widget.pageSize; _currentPage = page; }); } finally { setState(() { _isLoading = false; }); } }
@override Widget build(BuildContext context) { return ListView.builder( itemCount: _allItems.length + (_hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == _allItems.length) { // Load more indicator if (!_isLoading && _hasMore) { // Trigger load more WidgetsBinding.instance.addPostFrameCallback((_) { _loadPage(_currentPage + 1); }); } return const Center( child: Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ), ); }
final item = _allItems[index]; return ListTile( title: Text(item['title'] ?? ''), subtitle: Text(item['description'] ?? ''), ); }, ); }}
Query Result Caching
Implement smart caching for frequently accessed data:
class QueryCache { static final Map<String, CachedQuery> _cache = {}; static const Duration _defaultTTL = Duration(minutes: 5);
static Signal<QueryResult>? getCached( String queryKey, Map<String, dynamic> query, ) { final cached = _cache[queryKey]; if (cached != null && !cached.isExpired) { return cached.signal; } return null; }
static void setCached( String queryKey, Signal<QueryResult> signal, { Duration? ttl, }) { _cache[queryKey] = CachedQuery( signal: signal, expiresAt: DateTime.now().add(ttl ?? _defaultTTL), ); }
static void clearExpired() { _cache.removeWhere((_, cached) => cached.isExpired); }
static void clear() { _cache.clear(); }}
class CachedQuery { final Signal<QueryResult> signal; final DateTime expiresAt;
CachedQuery({required this.signal, required this.expiresAt});
bool get isExpired => DateTime.now().isAfter(expiresAt);}
// Cached query widgetclass CachedInstantBuilder extends StatelessWidget { final Map<String, dynamic> query; final String? cacheKey; final Duration? cacheTTL; final Widget Function(BuildContext, Map<String, dynamic>?) builder;
const CachedInstantBuilder({ super.key, required this.query, this.cacheKey, this.cacheTTL, required this.builder, });
@override Widget build(BuildContext context) { final db = InstantProvider.of(context); final key = cacheKey ?? query.toString();
// Try to get from cache first var querySignal = QueryCache.getCached(key, query);
if (querySignal == null) { // Create new query and cache it querySignal = db.subscribeQuery(query); QueryCache.setCached(key, querySignal, ttl: cacheTTL); }
return Watch((context) { final result = querySignal!.value; return builder(context, result.data); }); }}
Memory Management
Efficient Widget Patterns
Optimize widget rebuilds and memory usage:
// ✅ Good: Specific data extractionclass OptimizedPostList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilderTyped<List<Post>>( query: { 'posts': { 'where': {'published': true}, 'limit': 50, } }, transformer: (data) => (data['posts'] as List) .map((json) => Post.fromJson(json)) .toList(), builder: (context, posts) { return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) => PostCard(post: posts[index]), ); }, ); }}
// ❌ Avoid: Processing data in build methodclass UnoptimizedPostList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilder( query: {'posts': {}}, builder: (context, data) { // Expensive operations in build method final posts = (data['posts'] as List) .where((p) => p['published'] == true) .map((json) => Post.fromJson(json)) .toList() ..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return ListView.builder( itemCount: posts.length, itemBuilder: (context, index) => PostCard(post: posts[index]), ); }, ); }}
Memory-Efficient List Rendering
Use ListView.builder for large datasets:
class LargeDataList extends StatelessWidget { @override Widget build(BuildContext context) { return InstantBuilder( query: { 'items': { 'orderBy': {'createdAt': 'desc'}, 'limit': 1000, // Large dataset } }, builder: (context, result) { final items = (result.data?['items'] as List? ?? []) .cast<Map<String, dynamic>>();
return ListView.builder( // Only builds visible items itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( key: Key(item['id']), // Important for performance title: Text(item['title']), subtitle: Text(item['description']), ); }, ); }, ); }}
Dispose Resources Properly
Always clean up resources to prevent memory leaks:
class ResourceAwareWidget extends StatefulWidget { @override State<ResourceAwareWidget> createState() => _ResourceAwareWidgetState();}
class _ResourceAwareWidgetState extends State<ResourceAwareWidget> { late final Signal<QueryResult> _querySignal; StreamSubscription? _subscription; Timer? _refreshTimer;
@override void initState() { super.initState(); final db = InstantProvider.of(context);
_querySignal = db.subscribeQuery({'items': {}});
// Listen to query changes _subscription = _querySignal.toStream().listen((result) { // Handle query updates });
// Periodic refresh _refreshTimer = Timer.periodic( const Duration(minutes: 5), (_) => _refreshData(), ); }
@override void dispose() { // Clean up all resources _subscription?.cancel(); _refreshTimer?.cancel(); super.dispose(); }
void _refreshData() { // Refresh implementation }
@override Widget build(BuildContext context) { return Watch((context) { final result = _querySignal.value; return YourWidget(data: result.data); }); }}
Sync Performance
Batch Operations Efficiently
Group related operations for better sync performance:
class BatchOperationManager { final InstantDB db; final List<Operation> _pendingOperations = []; Timer? _batchTimer;
static const Duration _batchDelay = Duration(milliseconds: 500); static const int _maxBatchSize = 50;
BatchOperationManager(this.db);
void addOperation(Operation operation) { _pendingOperations.add(operation);
if (_pendingOperations.length >= _maxBatchSize) { _flushBatch(); } else { _scheduleBatch(); } }
void _scheduleBatch() { _batchTimer?.cancel(); _batchTimer = Timer(_batchDelay, _flushBatch); }
Future<void> _flushBatch() async { if (_pendingOperations.isEmpty) return;
final batch = List<Operation>.from(_pendingOperations); _pendingOperations.clear(); _batchTimer?.cancel();
try { await db.transact(batch); } catch (e) { // Handle batch error - could retry or log print('Batch operation failed: $e'); } }
void dispose() { _batchTimer?.cancel(); if (_pendingOperations.isNotEmpty) { _flushBatch(); } }}
// Usageclass BatchedCRUD { final BatchOperationManager _batchManager;
BatchedCRUD(InstantDB db) : _batchManager = BatchOperationManager(db);
void createMultipleItems(List<Map<String, dynamic>> items) { for (final item in items) { _batchManager.addOperation( db.create('items', { 'id': db.id(), ...item, }).first, ); } }
void updateMultipleItems(List<MapEntry<String, Map<String, dynamic>>> updates) { for (final update in updates) { _batchManager.addOperation( db.update(update.key, update.value), ); } }}
Connection Pool Management
Optimize WebSocket connections:
class ConnectionManager { static const Duration _reconnectDelay = Duration(seconds: 2); static const Duration _heartbeatInterval = Duration(seconds: 30); static const int _maxReconnectAttempts = 5;
final InstantDB db; int _reconnectAttempts = 0; Timer? _heartbeatTimer;
ConnectionManager(this.db) { _startHeartbeat(); _monitorConnection(); }
void _startHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic(_heartbeatInterval, (_) { _sendHeartbeat(); }); }
void _sendHeartbeat() { // Implementation depends on sync engine capabilities // This is conceptual try { db.syncEngine?.ping(); _reconnectAttempts = 0; // Reset on successful ping } catch (e) { print('Heartbeat failed: $e'); _handleConnectionLoss(); } }
void _monitorConnection() { db.syncEngine?.connectionStatus.stream.listen((isConnected) { if (!isConnected) { _handleConnectionLoss(); } else { _handleConnectionRestored(); } }); }
void _handleConnectionLoss() { if (_reconnectAttempts < _maxReconnectAttempts) { Timer(_reconnectDelay * (_reconnectAttempts + 1), () { _attemptReconnect(); }); } else { print('Max reconnect attempts reached'); } }
void _handleConnectionRestored() { _reconnectAttempts = 0; _startHeartbeat(); }
void _attemptReconnect() { _reconnectAttempts++; try { db.syncEngine?.connect(); } catch (e) { print('Reconnect attempt $_reconnectAttempts failed: $e'); } }
void dispose() { _heartbeatTimer?.cancel(); }}
UI Performance
Optimized Presence Updates
Throttle presence updates to avoid excessive network traffic:
class ThrottledPresenceManager { final InstantRoom room; Timer? _cursorTimer; Timer? _typingTimer;
static const Duration _cursorThrottle = Duration(milliseconds: 100); static const Duration _typingDebounce = Duration(milliseconds: 300);
ThrottledPresenceManager(this.room);
void updateCursor(double x, double y) { _cursorTimer?.cancel(); _cursorTimer = Timer(_cursorThrottle, () { room.updateCursor(x: x, y: y); }); }
void setTyping(bool isTyping) { _typingTimer?.cancel();
if (isTyping) { room.setTyping(true); _typingTimer = Timer(_typingDebounce, () { room.setTyping(false); }); } else { room.setTyping(false); } }
void dispose() { _cursorTimer?.cancel(); _typingTimer?.cancel(); }}
Efficient Cursor Rendering
Optimize cursor rendering for many users:
class OptimizedCursorLayer extends StatelessWidget { final InstantRoom room; final Widget child; static const int _maxVisibleCursors = 10;
const OptimizedCursorLayer({ super.key, required this.room, required this.child, });
@override Widget build(BuildContext context) { return Stack( children: [ child, Watch((context) { final cursors = room.getCursors().value;
// Limit visible cursors for performance final visibleCursors = _getLimitedCursors(cursors);
return RepaintBoundary( child: Stack( children: visibleCursors.map((entry) { return Positioned( left: entry.value.x, top: entry.value.y, child: CursorWidget( key: Key(entry.key), cursor: entry.value, ), ); }).toList(), ), ); }), ], ); }
List<MapEntry<String, dynamic>> _getLimitedCursors( Map<String, dynamic> allCursors, ) { final entries = allCursors.entries.toList();
// Sort by last update time entries.sort((a, b) { final aTime = a.value.data['lastUpdate'] ?? 0; final bTime = b.value.data['lastUpdate'] ?? 0; return bTime.compareTo(aTime); });
return entries.take(_maxVisibleCursors).toList(); }}
class CursorWidget extends StatelessWidget { final dynamic cursor;
const CursorWidget({super.key, required this.cursor});
@override Widget build(BuildContext context) { // Wrap in RepaintBoundary for better performance return RepaintBoundary( child: Container( width: 20, height: 20, decoration: BoxDecoration( color: cursor.data['color'] ?? Colors.blue, shape: BoxShape.circle, ), ), ); }}
Monitoring and Analytics
Performance Monitoring
Track performance metrics:
class PerformanceMonitor { static final Map<String, PerformanceMetric> _metrics = {};
static void startTimer(String operation) { _metrics[operation] = PerformanceMetric( operation: operation, startTime: DateTime.now(), ); }
static void endTimer(String operation) { final metric = _metrics[operation]; if (metric != null) { metric.endTime = DateTime.now(); _logMetric(metric); } }
static void _logMetric(PerformanceMetric metric) { final duration = metric.duration; print('Performance: ${metric.operation} took ${duration.inMilliseconds}ms');
// Send to analytics service _sendToAnalytics(metric); }
static void _sendToAnalytics(PerformanceMetric metric) { // Implementation for your analytics service // e.g., Firebase Performance, custom analytics }
static Future<T> measureAsync<T>( String operation, Future<T> Function() action, ) async { startTimer(operation); try { return await action(); } finally { endTimer(operation); } }
static T measure<T>( String operation, T Function() action, ) { startTimer(operation); try { return action(); } finally { endTimer(operation); } }}
class PerformanceMetric { final String operation; final DateTime startTime; DateTime? endTime;
PerformanceMetric({ required this.operation, required this.startTime, });
Duration get duration => endTime!.difference(startTime);}
// Usageclass PerformanceAwareService { Future<void> performExpensiveOperation() async { await PerformanceMonitor.measureAsync('expensive_operation', () async { // Your expensive operation here await Future.delayed(const Duration(seconds: 2)); }); }}
Memory Usage Tracking
Monitor memory consumption:
class MemoryMonitor { static void logMemoryUsage(String context) { // Get current memory usage (implementation varies by platform) final rss = _getCurrentMemoryUsage();
print('Memory usage at $context: ${rss}MB');
if (rss > _getMemoryThreshold()) { print('WARNING: High memory usage detected'); _handleHighMemoryUsage(); } }
static double _getCurrentMemoryUsage() { // Implementation depends on platform // This is a placeholder return 0.0; }
static double _getMemoryThreshold() { // Define your memory threshold (e.g., 200MB) return 200.0; }
static void _handleHighMemoryUsage() { // Clear caches, dispose unused resources, etc. QueryCache.clear(); _forceGarbageCollection(); }
static void _forceGarbageCollection() { // Force garbage collection if supported // Implementation varies by platform }}
Best Practices
1. Use Specific Queries
Always filter data at the query level:
// ✅ Good: Filter in queryfinal activeUsers = db.subscribeQuery({ 'users': { 'where': {'status': 'active'}, 'limit': 100, }});
// ❌ Avoid: Filter in UIfinal allUsers = db.subscribeQuery({'users': {}});// Then filtering in build method
2. Implement Proper Pagination
Don’t load all data at once:
// ✅ Good: Paginated loadingvoid loadNextPage() { final offset = currentPage * pageSize; final query = { 'items': { 'limit': pageSize, 'offset': offset, } };}
3. Batch Related Operations
Group operations for efficiency:
// ✅ Good: Batch operationsawait db.transact([ ...db.create('post', postData), db.update(authorId, {'postCount': {'\$increment': 1}}), ...db.create('notification', notificationData),]);
// ❌ Avoid: Separate transactionsawait db.transact([...db.create('post', postData)]);await db.transact([db.update(authorId, {'postCount': {'\$increment': 1}})]);await db.transact([...db.create('notification', notificationData)]);
4. Use Keys for List Items
Always provide keys for dynamic lists:
ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ListTile( key: Key(item['id']), // Important for performance title: Text(item['title']), ); },)
5. Profile and Measure
Regularly profile your app:
void main() { // Enable performance overlay in debug mode if (kDebugMode) { debugProfileBuildsEnabled = true; }
runApp(MyApp());}
Performance Checklist
Use this checklist to ensure optimal performance:
- Queries use specific filters and limits
- Large lists use ListView.builder with keys
- Resources are properly disposed
- Batch operations are used where possible
- Presence updates are throttled
- Memory usage is monitored
- Performance metrics are tracked
- Caching is implemented appropriately
- UI rebuilds are minimized
- Network requests are optimized
Next Steps
Learn more about optimizing your InstantDB Flutter app:
- Troubleshooting - Debugging performance issues
- Migration Strategies - Upgrading performant apps
- Offline Functionality - Optimizing offline performance
- Real-time Sync - Sync performance optimization