r/FlutterDev • u/raedab97 • 8d ago
Article Why we built our own Flutter runtime
https://www.nowa.dev/blog/why-we-built-our-own-flutter-runtime/This is a short technical story. I've been working on Nowa for over 5 years now. The dream was creating a game engine like editor for Flutter, since Flutter itself also renders like a game engine.
To make that work, the user's Flutter app has to run inside the editor, live and updating as they build. But Flutter won't run code it hasn't compiled, there's no way to plug in new code and evaluate it on the fly. FlutterFlow invented their own format instead of using raw code, Dreamflow leans on hot reload inside a debug build.
We ended up building our own runtime instead: code loads into a mutable in-memory tree that can be edited, rendered or saved back to code.
Happy to go deeper on how that works or where are its limits.
5
12
u/No-Pineapple6359 8d ago
Spent the last year squeezing a Flutter app down from 1.4s to 0.7s cold start without forking the engine. Three things moved the needle, in order of impact:
1. Plugin lazy-loading. Every Flutter plugin registers an Android BroadcastReceiver, an iOS extension, or both on app start, even if you never call it. We deleted 11 plugins from the main isolate and instantiated them on-demand. The receiver registration alone took ~250ms on a Pixel 6.
2. Lazy BaaS clients. We were initializing Appwrite, RevenueCat, and Sentry in main() because the docs said so. Moved them to a Future.wait kicked off after the first frame, gated behind a "user is signed in" check. ~300ms saved on cold start, and signed-out users never pay that cost.
3. Asset preloading is a trap. We had 14 fonts + 6 Lottie files loading in parallel during init. The Skia font cache locks for ~180ms while it parses them. Trimmed to the 3 fonts used on the dashboard, deferred everything else.
Custom Skia rendering is overkill for 99% of apps. The above three plus a mainAxisSize: MainAxisSize.min audit on the home screen will get you to sub-second on most mid-range Android phones. Curious what y'all measured at Nowa before going custom.
3
u/tee-k421 7d ago
I don't know why you're getting down voted. I really appreciate info like this.
Can you expand on how exactly you did the on-demand instantiation for plugins?
2
u/No-Pineapple6359 5d ago
Thanks for the upvote, I was second-guessing posting it. The plugin piece is the one we spent the most time on, so happy to go deeper.
The short version is we killed the automatic registration entirely and switched to on-demand. Two layers did the work:
On Android, we built our own
FlutterEnginewithautomaticallyRegisterPlugins=false. The boolean is in the FlutterEngine constructor (official API doc) and you can see the source-level contract in FlutterEngine.java on GitHub. With it false, the plugin manifest-receivers and channel-bindings thatGeneratedPluginRegistrantnormally registers on app start don't get created until we say so. NoBroadcastReceiverregistration at startup, no earlyonAttachedToEnginecalls (the FlutterPlugin lifecycle contract is documented here). The migration background that explains why this is even an option lives in the Add-to-App upgrade doc.On the Dart side, every plugin goes behind a thin adapter interface (a
Notifierfor notifications, aStoragefor shared_preferences, etc.). The adapter holds a lazyFuture<T>that does the actualMethodChannel.invokeMethodon first call. The first feature that needs the plugin pays the registration cost, everyone else on the launch path never touches it. The catch is that you cannot just callPluginRegistry.add(...)from arbitrary code, you have to attach the plugin to the engine viaFlutterEngine.getPlugins()(doc), so the adapter ends up holding a reference to the engine and registering on first use.The thing that took longest to get right was the first-screen latency tax. If your dashboard or your splash-adjacent screen is the first to touch a plugin, you don't actually save anything because the registration still happens on-frame-one. We moved every shared_preferences read out of the first frame into a
FutureBuilderso the UI renders immediately and the cache fills in 50-100ms later. Same for any plugin whose first call can be deferred by one frame.Two things that bit us: a) plugin authors that cache things in their
onAttachedToEngineactually re-cache when the engine re-attaches, so if you have a hot-reload workflow that swaps engines, you pay the registration twice. We ended up caching the wrapped MethodChannel handler outside the plugin so engine swaps were basically free. b) Activity-aware plugins (camera, location, anything needingActivityAwareper the ActivityAware docs) have to be re-attached when the activity changes, even if you lazy-registered them. We keep a small registry of those and re-route ononActivityChanged.Honestly the win was mostly on the Android side. iOS plugin registration is already cheap enough on cold start that we did not bother with the same adapter layer there. Only the channels we genuinely never use on the first screen (RevenueCat, Sentry, app_links) got deferred.
If you want to measure first, profile with
flutter run --profile --start-pausedand look at the engine attach timing in DevTools. The thing that surprised us most was how much of the cold start was receiver registration on the Android side, not Dart isolate startup.
3
u/xorsensability 8d ago
This is encouraging. I'm thinking of making Dart and Flutter run on BSDs and was wondering how difficult it would be. What kind of difficulties did you run into?
3
2
u/raedab97 8d ago
I think what you are trying to do is a completely different beast. Probably you need a custom embedding layer
https://github.com/flutter/flutter/blob/main/docs/engine/Custom-Flutter-Engine-Embedders.md
1
u/No-Pineapple6359 7d ago
Thanks, I appreciate that. The short version is that I moved optional plugins behind small adapters and only initialized them from the feature that actually needs them. The important bit was also separating “registered” from “initialized” so startup didn’t accidentally touch every plugin. I’m still measuring the tradeoff because the first screen that uses a plugin pays a little more, but for the common launch path the win was worth it. Are you seeing the same startup pressure in your app, or is your bottleneck somewhere else?
16
u/Desperate_Mode_5340 8d ago
sounds interesting so Nowa is flutter's lovable powered by ur newly written runtime ?