← Flutter Reference

Riverpod vs BLoC

State Management

By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.

Quick answer

For 99% of apps, performance is not the deciding factor between Riverpod and BLoC — both rebuild only what watches changed state. The measurable differences are at the margins: BLoC's stream-based event pipeline has slightly more overhead per emission, while Riverpod's fine-grained select gives more surgical rebuild control. At 5M users with feed scrolling, infinite lists, and real-time sync, neither has been a performance bottleneck for us.

Choose on architecture and team fit, not micro-benchmarks. If you are hitting rebuild problems, the fix is widget granularity and select/buildWhen, not swapping your state library.

Feature comparison

MetricRiverpodBLoC
Rebuild granularityselect() — slice-levelbuildWhen — per-builder
Per-emission overheadVery lowLow (stream pipeline)
High-frequency events (60Hz)Handle nativelyUse bloc_concurrency
Auto-cleanupautoDispose modifierManual (BlocProvider lifecycle)
Large-list friendlinessGood with selectGood with buildWhen
Memory for transient stateExcellent (autoDispose)Good (manual scope)
DevTools rebuild trackingProvider listBlocObserver transitions
Winner for raw micro-perfMarginalMarginal
Real bottleneck?Widget rebuild costWidget rebuild cost

Detailed comparison

Rebuild granularity

Both libraries rebuild only widgets that observe changed state — but the controls differ.

Riverpod's select lets a widget watch a slice of a provider's state and rebuild only when that slice changes:

final userName = ref.watch(userProvider.select((u) => u.name));

BLoC's buildWhen on BlocBuilder does the same:

BlocBuilder<UserBloc, UserState>(
  buildWhen: (prev, curr) => prev.name != curr.name,
  builder: (_, s) => Text(s.name),
);

Functionally equivalent. Riverpod's select is slightly more ergonomic and composable across multiple providers; BLoC's buildWhen is per-builder. Neither has a meaningful perf edge here.

Stream overhead (BLoC)

BLoC emits states through an EventSink → transformer → Stream<State> pipeline. Each event flows through on<Event>, an emitter, and the stream before listeners rebuild. For high-frequency events (scrolling, drag updates, sensor streams at 60Hz) that pipeline adds micro-overhead per event.

In practice, I have only seen this matter for sensor/joystick input at 60+ Hz, where collapsing events with bloc_concurrency (droppable or restartable) or moving to a raw ValueNotifier for that one stream is the fix. For normal UI, it is invisible.

Provider overhead (Riverpod)

Riverpod providers are lightweight objects with a dependency graph. Reading a provider is cheap; the cost is in how many widgets rebuild, not in the provider mechanism. autoDispose providers clean up when no longer watched, keeping memory bounded — useful for per-item providers in long lists.

Large-list scrolling

This is where people wrongly blame state management. A janky infinite list is almost always a widget rebuild cost problem, not a BLoC/Riverpod problem. The fixes are identical regardless of library: ListView.builder (not Column), const constructors, RepaintBoundary around heavy items, select/buildWhen to avoid rebuilding the whole row when one field changes, and keys on items.

At iStoria we render feed lists of 50+ module cards with BLoC and the jank disappeared once we added buildWhen and RepaintBoundary — not after considering a library swap.

Memory footprint

BLoC blocs stay alive as long as their BlocProvider is in the tree; you manage lifecycle manually. Riverpod's autoDispose modifier frees providers automatically when no widget watches them, which is friendlier for transient state. For long-lived app state, both are comparable in memory.

Cold start and initialization

There is a subtle difference at app launch. BLoC initializes eagerly when BlocProvider(create:) runs in the widget tree, so you control exactly when each bloc spins up by where you place the provider. Riverpod providers are lazy by default — they are created on first ref.watch/ref.read — which means less wasted initialization for features the user has not visited yet. For a 50-module app where not every screen is reached in a session, lazy initialization is a real cold-start win. You can make Riverpod eager with keepAlive, and you can make BLoC lazy with lazy: true on BlocProvider, so neither is locked in — but the defaults favor Riverpod for startup cost.

Code comparison

Rebuild a single field — Riverpod

class UserNotifier extends Notifier<User> {
  @override
  User build() => User(name: '', avatar: '', bio: '');
  void updateBio(String b) => state = state.copyWith(bio: b);
}

// only rebuilds when `name` changes — avatar/bio updates skip this widget
final name = ref.watch(userProvider.select((u) => u.name));

Rebuild a single field — BLoC

BlocBuilder<UserBloc, UserState>(
  buildWhen: (p, c) => p.name != c.name,
  builder: (_, s) => Text(s.name),
);

Both achieve the same surgical rebuild. The performance lesson: neither library is your bottleneck. Profile widget rebuilds with the Flutter Performance overlay and DevTools before touching your state architecture.

Which should you choose?

Choose Riverpod for perf when: you have many transient providers and want automatic cleanup (autoDispose), or you prefer select ergonomics for fine-grained rebuilds. Also the default if you simply prefer Riverpod's API.

Choose BLoC for perf when: you need event transformers (bloc_concurrency) to debounce/dedupe high-frequency events, or your team already standardized on BLoC. Performance alone almost never justifies a swap either direction.

Related definitions

Related reading

FAQ

Is BLoC slower because of streams?

Marginally per emission, but not measurably for normal UI. The stream pipeline adds microseconds. It only matters at high-frequency event rates (60Hz+ input), where bloc_concurrency transformers collapse events anyway.

Which uses less memory?

Roughly comparable for long-lived state. Riverpod's autoDispose gives better memory behavior for transient/per-item providers because they free automatically. BLoC requires manual lifecycle management via BlocProvider scoping.

My list scrolls janky — should I switch state libraries?

No. Switch to ListView.builder, add const constructors, wrap heavy items in RepaintBoundary, and use select/buildWhen to avoid rebuilding whole rows. Jank in lists is a widget-rebuild problem, not a state-management problem.

Does Riverpod's autoDispose have a performance cost?

autoDispose providers tear down their state when no listener watches them, and rebuilding them on re-subscription has a small cost proportional to what the provider does. For trivial providers (a computed value, a filter flag) that cost is negligible. For expensive providers (a stream subscription, a heavy computation) it can matter — in that case keep the provider alive longer (a keepAlive link or a parent that stays subscribed) rather than disabling autoDispose globally. The default is good; tune the exceptions.

How do I profile which widgets rebuild?

Use the Flutter DevTools Performance overlay and the 'Track widget rebuilds' option in the inspector. Both Riverpod and BLoC show up as rebuild sources there. If a widget rebuilds more than expected, narrow the watch with select or buildWhen before considering structural changes. The profiler tells you what rebuilds; your job is to make it rebuild less.


Available for hire. Abdelrahman Saed is a Senior Mobile Engineer (Flutter) — open to full-time, fractional, contract, or advisory work. Hire me →

Book a 20-minute call · Download the CV (PDF) · See how I work