Common Performance Pitfalls
Understand the most common mistakes that lead to unnecessary rebuilds, excessive memory usage, and inefficient state management in Riverpod applications.
What is it?
Performance pitfalls are patterns that make a Riverpod application slower or less efficient than necessary.
Most Riverpod applications perform well without special optimization. However, certain coding patterns can cause:
- Unnecessary widget rebuilds
- Repeated network requests
- Excessive provider recomputations
- Memory leaks
- Poor application responsiveness
Knowing these pitfalls helps you write scalable applications from the beginning.
Why does it exist?
Riverpod is designed to update only the parts of an application that need to change.
Poor provider design or incorrect API usage can prevent Riverpod from taking advantage of its reactive architecture.
Understanding these common mistakes helps you:
- Reduce rebuilds
- Improve memory usage
- Avoid duplicate work
- Keep applications responsive
- Build maintainable state management
Syntax
Prefer watching only required state
final userName = ref.watch(
userProvider.select((user) => user.name),
);
Explanation:
select()listens only to the required property.- The widget rebuilds only when
namechanges.
Split large providers
final userProvider =
NotifierProvider<UserNotifier, User>(
UserNotifier.new,
);
final cartProvider =
NotifierProvider<CartNotifier, Cart>(
CartNotifier.new,
);
Explanation:
- Separate providers reduce unnecessary rebuilds.
- Each provider has a single responsibility.
Dispose temporary providers
final searchProvider =
FutureProvider.autoDispose<List<Product>>((ref) async {
return repository.search();
});
Explanation:
.autoDisposefrees resources when the provider is no longer used.- Temporary state does not remain in memory.
Mental Model
Good provider architecture:
Feature
│
├── UserProvider
├── CartProvider
├── ThemeProvider
└── SettingsProvider
Small updates
│
▼
Small rebuilds
Poor provider architecture:
AppProvider
│
Everything depends on it
│
Large rebuilds
Think of providers as independent building blocks rather than one central store.
Examples
Simple Example
final count = ref.watch(counterProvider);
Explanation:
- Only widgets using
counterProviderrebuild when the counter changes.
Real-World Example
class Dashboard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Column(
children: [
UserHeader(),
WeatherCard(),
NewsFeed(),
NotificationPanel(),
],
);
}
}
Explanation:
- Each child watches its own provider.
- Updating one section does not rebuild the others.
When to Use
Review these practices when:
- Optimizing large applications.
- Investigating unnecessary rebuilds.
- Improving memory usage.
- Working with frequently changing state.
- Building feature-rich dashboards.
- Profiling application performance.
When NOT to Use
Avoid premature optimization.
Riverpod is already efficient by default.
Measure performance before introducing complex optimizations, and prioritize readable code unless a bottleneck has been identified.
Best Practices
- Keep providers focused on one responsibility.
- Build small, reusable widgets.
- Use
select()when only part of the state is needed. - Prefer immutable state models.
- Use
.autoDisposefor temporary state. - Invalidate providers instead of recreating them manually.
- Cache expensive operations inside providers.
- Profile before optimizing.
Common Mistakes
Watching more state than necessary
Wrong:
final user = ref.watch(userProvider);
return Text(user.name);
Explanation:
- The widget rebuilds whenever any field of
Userchanges.
Correct:
final name = ref.watch(
userProvider.select((user) => user.name),
);
return Text(name);
Explanation:
- Only changes to
nametrigger a rebuild.
Creating one massive provider
Wrong:
final appProvider =
NotifierProvider<AppNotifier, AppState>(
AppNotifier.new,
);
Explanation:
- Unrelated state becomes tightly coupled.
- Small changes can rebuild large parts of the UI.
Correct:
final authProvider =
NotifierProvider<AuthNotifier, AuthState>(
AuthNotifier.new,
);
final settingsProvider =
NotifierProvider<SettingsNotifier, Settings>(
SettingsNotifier.new,
);
Explanation:
- Each provider owns a single concern.
- Updates remain localized.
Refreshing providers unnecessarily
Wrong:
ref.refresh(productsProvider);
Explanation:
- Forces immediate recomputation.
- May trigger unnecessary API calls.
Correct:
final products = ref.watch(productsProvider);
Explanation:
- Reuse cached state until new data is actually required.
Forgetting .autoDispose
Wrong:
final searchProvider =
FutureProvider<List<Product>>((ref) async {
return repository.search();
});
Explanation:
- Temporary search results remain alive longer than necessary.
Correct:
final searchProvider =
FutureProvider.autoDispose<List<Product>>((ref) async {
return repository.search();
});
Explanation:
- Temporary state is automatically cleaned up when unused.
Performing side effects inside build()
Wrong:
Widget build(BuildContext context, WidgetRef ref) {
ref.read(loggerProvider).log('Opened page');
return const HomeView();
}
Explanation:
build()can run many times.- Side effects may execute repeatedly.
Correct:
ref.listen(userProvider, (previous, next) {
logger.log('User updated');
});
Explanation:
ref.listen()is intended for reacting to state changes without rebuilding the UI.
Related APIs
ref.watch()ref.read()ref.listen()select().autoDisposeref.invalidate()ref.refresh()- Provider
- NotifierProvider
- AsyncNotifierProvider
Summary
Most Riverpod performance problems come from application architecture rather than the framework itself. By creating focused providers, minimizing rebuilds, using select() appropriately, leveraging automatic caching, disposing temporary state, and avoiding unnecessary recomputation, you can build Riverpod applications that remain responsive and scalable as they grow.