Very slow performance on debug mode
Is there an existing issue?
- [x] I have searched existing issues
Build info
- objectbox version: 4.3.0
- Flutter/Dart version: 3.29.3
- Build OS: macOS 15.4.1 (24E263)
- Deployment OS or device: Android 14,Android 15, iOS 18.4
I'm migrating from Isar to ObjectBox and facing a major issue in debug mode. I’ve done some investigation but haven’t identified the root cause yet.
I’m already using ObjectBox in another app (Flutter version 3.24.5) without any performance issues during debugging. However, in this other (much larger) app, I’m unable to run it smoothly in debug mode. After some research, I found two possible reasons:
-
Merged platform and UI threads – Flutter issue #150525. This specific comment caught my attention.
-
Hot reload performance degradation when using multiple isolates – Flutter issue #169437
I’ve tried using the following flag:
<meta-data
android:name="io.flutter.embedding.android.DisableMergedPlatformUIThread"
android:value="true" />
But the result was the same.
My question is: is this a Dart/Flutter limitation, or is it truly related to the merged thread behavior and FFI communication?
It's also worth noting that I don’t experience this problem with Isar, even though it also relies on FFI.
My app follows an offline-first approach, so on the home screen it performs (or attempts to perform) two separate synchronization processes. Each of these triggers its own isolate via ObjectBox, which leads to noticeable performance degradation.
Thanks for reporting and making us aware of the merged threads change. At this time I don't fully understand if this might have an impact when using ObjectBox APIs, so we will have to look into this when there is time.
However, the merged threads change is only enabled since Flutter SDK 3.32. Yet, you wrote you are using Flutter SDK 3.29?
In that case what exactly do you mean by "unable to run it smoothly in debug mode"? You should be able to tell if performance degrades after doing a hot reload or at some other point?
However, the merged threads change is only enabled since Flutter SDK 3.32. Yet, you wrote you are using Flutter SDK 3.29?
Actually, thread merging was introduced in version 3.29 but only became the default in version 3.32.
In that case what exactly do you mean by "unable to run it smoothly in debug mode"? You should be able to tell if performance degrades after doing a hot reload or at some other point?
After migrating to ObjectBox, hot reload performance became noticeably slower. My app runs two queries during startup and performs additional queries only on user demand. I’m not sure if the slowdown is related to the overall size of the app, but it shouldn’t be, considering the amount of data is relatively small compared to another project I’m working on, which also uses ObjectBox with Flutter (version 3.24) and doesn’t suffer from this issue—even though that project has periods of much heavier query usage.
Most queries should perform in a few milliseconds (or sub-milliseconds) - if they have a proper index. Maybe you can pinpoint the slowdown, e.g. by performance logging (measure the duration of some action and log it) to give us a clearer picture.
@greenrobot As I mentioned, the app runs two queries during startup:
For the first one, I’m using the query builder:
Query<T>? query;
try {
query = store.box<T>().query(condition).build();
if (limit != null && limit > 0) {
query.limit = limit;
}
return query.find();
} catch (e) {
throw const PersistenceException();
} finally {
query?.close();
}
Execution time
[log] Start find Punch - 2025-06-17T09:42:32.360172 - Status: PunchLocalStatus.synchronized - Limit: none
[log] End find Punch - 2025-06-17T09:42:32.366161 - Execution time: 5 ms
For the second one, I’m simply using getAllAsync:
try {
return store.box<T>().getAllAsync();
} catch (e) {
throw const PersistenceException();
}
Execution time
[log] Start find Activity - 2025-06-17T09:42:33.402156
[log] End find Activity - 2025-06-17T09:42:40.110498
[log] Activity query execution time: 6708 ms
The issue itself seems to be with getAllAsync(). I'll refactor the code to see what the performance impact is.
The issue itself seems to be with getAllAsync()
@Mairramer How many objects do you have?
The issue itself seems to be with getAllAsync()
@Mairramer How many objects do you have?
It is empty.
please double check that - it would be extremely surprising for a store.box<T>().getAllAsync() without any object to take more than a few milliseconds. There's no way this takes 6 seconds - maybe something else is interfering or you are measuring not just "get all".
please double check that - it would be extremely surprising for a
store.box<T>().getAllAsync()without any object to take more than a few milliseconds. There's no way this takes 6 seconds - maybe something else is interfering or you are measuring not just "get all".
Could the AppFileModel relationship be causing interference?
@Backlink('activity')
final ToMany<AppFileModel> appFiles = ToMany();
@Entity()
class AppFileModel {
@Id()
int? id;
final String? name;
final String? extensionName;
final String? path;
final String? base64;
@Property(type: PropertyType.byteVector)
final List<int>? bytes;
}
If there really are no objects, relations will not matter.
It'd be helpful if you could actually pinpoint the issue. How do you know it is related to ObjectBox? We'd need some hard data to continue.
If I simply comment out the getAllAsync() call and return an empty list, there are no freezes and hot reload becomes significantly faster. The issue might be related to this: https://github.com/flutter/flutter/issues/169437.
It’s probably caused by this relationship, since getAllAsync() will open a separate isolate.
I'm also a little surprised, I've used isolates in other contexts but this is the first time I've seen this problem.
I could switch to using store.box<T>().getAll() instead, considering that this store is not expected to hold a large amount of data. Based on user metrics and usage frequency, the data volume should remain low, even though this feature is core for some customers.
I could switch to using store.box<T>().getAll() instead
I just wanted to suggest the same... 🙂 Performance wise this should be fine for a couple thousand objects (milliseconds).
I would like to say that I see the same issue with my app. Its a "medium size" app and I use isolates like Author does.
I lack a lot of information but I will share what I know:
- It wasnt always like that, I dont know when, but at some point it got slower running in debug;
- It doesnt affect the release mode, in release mode it has been the same in the last 2 years;
I encountered the same problem. I had to replace findAsync() with find(), otherwise the requests could wait up to 2 seconds. But as soon as I switch to find(), the request is immediately executed in 1-2 ms.
In my case I run:
ob.Box<Message> messageBox = main.objectbox.box<Message>();
final messageQuery = messageBox
.query(
ob.Message_.from
.equals(uid)
.or(ob.Message_.to.equals(uid))
.and(ob.Message_.date.greaterOrEqualDate(minDate!)),
)
.order(ob.Message_.id)
.build();
List<Message> foundedMessages = messageQuery.find(); // <-- findAsync() slower
messageQuery.close();
I’m encountering the same problem (tested on Flutter 3.32.6 & 3.35.3).
In my app I use findAsync for all queries. On debug builds the app sometimes freezes at startup when multiple findAsync calls run in parallel. In the debugger I can see several Store._callFunctionWithStoreInIsolate entries stuck.
From what I understand, each async ObjectBox call spawns and tears down a fresh isolate. With Flutter’s newer default (platform/UI threads merged), rapidly creating/disposing isolates seems to cause UI jank. My app uses Riverpod, so during the first build it may trigger multiple queries at once.
As a temporary workaround I reverted Flutter to the old threading model by adding these flags in
<meta-data android:name="io.flutter.embedding.android.EnablePlatformIsolates"
android:value="false" />
<meta-data android:name="io.flutter.embedding.android.DisableMergedPlatformUIThread"
android:value="true" />
This reduced the freezes/ANRs in my case.
Suggestion: Instead of spawning a new isolate for every async query, it might be better if ObjectBox used a shared long-lived isolate (or a thread pool) for async operations. That would avoid isolate churn and reduce the risk of UI jank with Flutter’s current threading defaults.
I'm seriously impressed how bad isolates are. 🤯
@lsthye thanks for this investigation and report!
it might be better if ObjectBox used a shared long-lived isolate (or a thread pool) for async operations
The downside of this is that this isolate will stay around eating up resources. Also if there are multiple operations at the same time, they won't run in parallel anymore?
I should check if the recommendations on using isolates have changed, now that the UI and platform threads have been merged. Maybe there is some kind of priority setting that should be used now? I can't imagine Dart and Flutter developers didn't think about multiple isolates spawning that take away required resources from the UI handler.
Thanks for the reply!
I understand the concern about a long-lived isolate keeping resources around, but according to Flutter’s own docs (https://docs.flutter.dev/perf/isolates#stateful-longer-lived-isolates) there is non-trivial performance overhead every time a new isolate is spawned.
That’s why they specifically recommend creating a long-lived isolate (via Isolate.spawn) when you need to repeatedly run work in an isolate, instead of spawning/tearing down for each call.
As I last checked, ObjectBox currently calls Isolate.spawn and then Isolate.exit for every async query. This means each async method pays the full isolate startup cost, which adds up if queries are frequent. A shared isolate could avoid that overhead by staying alive across queries.
Regarding parallelism:
-
Multiple async calls can still be queued into the same isolate’s event loop, so they don’t block the UI thread.
-
If true parallelism is required (e.g. CPU-bound work), separate isolates could still be spawned explicitly. But for typical DB queries, a single long-lived background isolate should be enough and more efficient.
So I think the trade-off is: a small fixed memory footprint for keeping one isolate alive, versus repeated spawn/teardown overhead that may hurt performance and responsiveness.
It might be worth benchmarking both approaches to see what’s best in practice, but Flutter’s docs do seem to lean toward reusing a long-lived isolate in cases like this.
Thanks for the input, all good and well. Regarding implementation details, I doubt we'll go with a long lived isolate. We already have async write ops on the native side. Since we are a multi-platform (e.g. also programming languages) lib we prefer solutions that provide value to all platforms and avoid major features that are platform specific like Dart isolates. E.g. we'd more likely provide native async read ops and hook up the callback to Dart. Details are still TBD.
On the other hand, this also seems to be a regression on the Dart side, so it might be that it gets fixed by Dart itself.
Thanks for clarifying! That makes a lot of sense. I agree that using ObjectBox's native async read ops and bridging the results back to dart would be the best long term solution. This also avoids the isolate management overhead on the dart side and also provides consistent value across all platforms.
I also see your point about avoiding platform specific mechanisms like dart isolates for core features. Having async support handled natively sounds cleaner, more efficient.
Looking forward to seeing how this evolves.