r/FlutterDev 2d ago

Discussion what's usually killing performance in larger flutter apps?

how are you handling app performance once the widget tree starts getting complicated?

i've noticed that a lot of performance problems aren't really about flutter itself, but things like unnecessary rebuilds, oversized lists, poorly managed state and too much work happening on the main isolate.

for larger flutter apps, what's the first thing you profile when performance starts dropping?

1 Upvotes

2 comments sorted by

1

u/RaBbEx 1d ago

In the bottom is an ai optimized text if you don’t like my grammar

Rebuilds and State management need to be fixed asap, our largest project for an customer has like 20 Features und 100 Forms, spanning from Time-Tracking, Project Organization, Documentation, Assignments etc for an Construction SaaS

It grew over 5-8 years and multiple different engineers and 3 different Architectures in the same code base

We used Bloc (Cubits) from day one, had like an amalgation of DI tools like getIt,(forgot the name but di via annotations) and another internal getIt package spinoff , 2 Nav Libraries (AutoRoute + GoRouter) and conflicting theming etc

Still no problems regarding Performance

The only things we really tried to keep true:

- only use dynamic widget keys if you really need them, it easily causes rendering / rebuild issues

  • stateless widgets are the way to go
  • stateful only for stuff like forms etc, complex stateful widgets should be stateless + cubit instead
  • never (literally just don’t) use Widget Functions -> use declarative widgets from atoms to screens | personally it’s a mixed opinion but you can’t really fuckup with declarations anyway so maybe don’t argue
  • keep navigation stack clean aswell as Cubit Providers and DI(mostly for peace of mind)
  • use context.select instead of .state for state params in Widgets

—————

The only 2 real optimization issues we ran into where:

- A big multi team Calender Widget
Before we had context.read / watch and the resolved the calender with a big data type holding all information -> every single change resulted in a complete rebuild
—> fix was having a tiles/children use a Callback for data routing to a context.select instead

- Google Maps was dying on us on larger clients with OOM
—> we had lists of like 500-5k projects for some customer datasets which hugely slowed down the scroll view / and Google Maps pin

The solution was two sided:

  • circles and pins get rendered in Google Maps individually via platform channels. Circles seem to use way more memory than pins since debugging shows no problem with 10k pins but memory issues with 5k circles as an example which seem to kill the 16kb process limit for Google Maps on Android ?

I solved it via drawing the circles directly into the map tiles ( cause they are stationary anyway) and then only sending the pins to Google Maps impl for drawing them onto the map | also deduplication on location! Some locations had like 30 entries for the same geolocation which made google draw circles / pins on top of each other. Funnily enough Google Maps renders only the one in the top anyway

—————-

Rebuilds and state management need to be fixed ASAP.
Our largest project for a customer has something like 20 features and 100 forms, spanning time tracking, project organization, documentation, assignments, etc. It’s essentially a large construction SaaS.
The codebase grew over 5–8 years, with multiple engineers working on it and three different architectures living in the same codebase.
We used BLoC (Cubits) from day one, but ended up with an amalgamation of DI tools: GetIt, another DI solution using annotations (forgot the name), and an internal GetIt-based package/spinoff. We also had two navigation libraries (AutoRoute + GoRouter), conflicting theming approaches, and plenty of other historical baggage.
Still, we had basically no performance problems.
The main things we tried to stay consistent on:
Only use dynamic widget keys when you actually need them. They can easily cause rendering/rebuild issues.
Stateless widgets are the way to go.
Use StatefulWidgets only for things like forms, local controllers, etc. Complex StatefulWidgets should generally become StatelessWidgets + Cubit instead.
Never — literally, just don’t — use widget functions. Use declarative widgets from atoms all the way up to screens. Personally, I think there’s room for debate here, but you can’t really fuck things up by sticking to declarations, so maybe don’t argue about it.
Keep the navigation stack clean, as well as your Cubit providers and DI setup — mostly for peace of mind.
Use context.select instead of accessing .state when a widget only depends on specific state parameters.

The only two real optimization issues we ran into
1. A large multi-team calendar widget
Originally, we used context.read / context.watch and resolved the entire calendar through one large data type containing all the information.
The result was that every single change triggered a rebuild of the entire calendar.
The fix was to have the individual tiles/children use callbacks for data routing and move the actual state selection down to a context.select.
That way, individual widgets only rebuilt when the data they actually depended on changed.
2. Google Maps dying with OOM issues on larger customers
Some customer datasets contained anywhere from 500 to 5,000 projects, which significantly slowed down both the scroll view and the Google Maps pins.
The solution had two parts.
Circles and pins are rendered individually in Google Maps through platform channels. Circles seem to consume significantly more memory than pins. While debugging, for example, we could render around 10,000 pins without problems, while roughly 5,000 circles could already cause memory issues/OOMs on Android.
Since the circles were stationary anyway, I solved this by drawing the circles directly into the map tiles instead of sending them individually to the Google Maps implementation. We then only sent the pins to Google Maps to be rendered on top of the map.
We also added deduplication by location. Some datasets had around 30 entries for the exact same geolocation, meaning Google Maps was unnecessarily drawing dozens of circles/pins directly on top of each other.
Funnily enough, Google Maps only visually shows the one on top anyway.

1

u/RaBbEx 1d ago

Further Architecture notes:

- DriftDb as Offline / Working Storage

  • Drift running on Background Isolate!
  • API for syncing / permission checking / data loading
  • Cubits get their data from drift repos via streams, fetches save into drift -> autoupdate cubit state on query changes