App Startup Optimization
4 min read
Performance
| Phase | What runs | Levers you control |
|---|---|---|
| 1. Native init | OS loads binary, starts Dart VM | Engine warmup (native splash), reduce APK/IPA size |
| 2. Flutter init | WidgetsFlutterBinding.ensureInitialized, framework boot | Minimal work before runApp |
| 3. App init | runApp(MyApp()), build tree, async setup | Defer non-critical init; show splash; lazy-load routes |
| 4. First frame | Layout + paint + raster | Keep first widget tree light; use const; precache critical images |
Two important metrics:
- Time to First Frame (TTFF) — when the user sees anything.
- Time to Interactive (TTI) — when they can actually tap and get a real response.
You optimise differently for each. Splash screens move TTFF down visually but don't help TTI.
Code in action — defer heavy init
void main() {
// Minimal main — show app first, init in background
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp()); // first frame ASAP
}
class _MyAppState extends State<MyApp> {
late final Future<void> _bootstrap = _bootstrap_();
Future<void> _bootstrap_() async {
await Firebase.initializeApp(); // can't be skipped
await Hive.initFlutter(); // local DB
await ConfigService.load(); // remote config
}
@override
Widget build(BuildContext context) => MaterialApp(
home: FutureBuilder(
future: _bootstrap,
builder: (ctx, snap) => snap.connectionState == ConnectionState.done
? const HomeScreen() // → TTI here
: const SplashScreen(), // → TTFF immediately
),
);
}
The native splash (configured in flutter_native_splash) covers phases 1–2; your Dart splash covers 3. Together the user never sees a blank screen.
Common levers, ordered by impact
| Lever | Typical impact |
|---|---|
| Lazy-init Firebase / SDKs in parallel after first frame | 200–600ms off TTFF |
flutter_native_splash (native splash matching app theme) | Visual TTFF appears ~instant |
Lazy-load routes (GoRoute(builder: ...) doesn't eagerly construct widgets) | Smaller initial tree |
compute(parseJson, raw) for big payloads | Frees main isolate for first frame |
precacheImage for above-the-fold images | No empty boxes flashing in |
Tree shaking + --obfuscate + --split-debug-info | Smaller binary → faster load |
const constructors everywhere | Skip rebuilds, reduce GC pressure |
| Remove unused fonts / icon glyphs | Smaller asset payload |
Measuring properly
# Cold-start trace; outputs timing JSON
flutter run --trace-startup --profile
# Output highlights:
# timeToFirstFrameMicros → TTFF
# timeToFrameworkInitMicros → phase 2 done
# timeToFirstFrameRasterizedMicros
Profile mode gives real timings. Debug mode timings are useless for startup numbers.
For ongoing monitoring, instrument with Firebase Performance Monitoring (custom traces for boot phases) or Sentry's app-start spans.
Common mistakes to avoid
❌ awaiting Firebase / DI / Hive in main() before runApp
Adds their full init time to TTFF. User sees blank screen / native splash longer.
✅ Init from your app widget's initState; show splash while it runs.
❌ Pre-loading the entire user dataset on launch
Big DB read = visible jank or stall.
✅ Stream data in as needed; show progressive UI.
❌ Decoding huge images on the UI isolate
First frame stalls; sometimes app appears to hang.
✅ precacheImage with cacheWidth/Height, or decode in an isolate.
❌ Measuring startup in debug mode
Debug adds asserts, lacks AOT, includes hot reload runtime.
✅ flutter run --trace-startup --profile
❌ Optimising what you didn't measure
Trace first, then fix. Otherwise you'll spend a day on Firebase init when
the real cost was image decode.
❌ Re-initialising things on hot restart
Your splash + bootstrap re-runs on every restart in dev. Don't conflate
that with cold start on a user's device.
Interview follow-ups
-
What's the difference between cold start, warm start, and hot start? Cold start: process not running — full native + Flutter + app init. Warm start: process running but activity destroyed — saves some VM init. Hot start: app in background — instant. Most "startup time" discussion is about cold start because it's worst-case.
-
Where does
flutter_native_splashfit in? It generates a native iOS launch screen / Android splash that matches your branding — shown during phases 1 and 2, before Flutter can render anything. Without it, users see a white screen (default) until your Dart splash renders. It's free TTFF improvement. -
How do you instrument app-start performance in production?
Firebase Performance Monitoringautomatically captures_app_starttraces. For finer control, add custom traces around your bootstrap stages (Firebase init, config fetch, first DB read). Sentry has similarappStartmeasurements. Aggregate over real-device populations — your dev machine is the fastest device any user will own. -
Why move JSON parsing off the UI isolate? Parsing a large JSON blob on the UI isolate blocks first frame for tens or hundreds of ms.
compute(parseJson, raw)runs it on a worker isolate, leaving the UI isolate free to render. Especially worth it for payloads >100KB or anything you need during boot.
How helpful was this content?
Please sign in to rate this article.