App Startup Optimization

Medium PriorityAsked in ~65% of senior interviews

4 min read

Performance

PhaseWhat runsLevers you control
1. Native initOS loads binary, starts Dart VMEngine warmup (native splash), reduce APK/IPA size
2. Flutter initWidgetsFlutterBinding.ensureInitialized, framework bootMinimal work before runApp
3. App initrunApp(MyApp()), build tree, async setupDefer non-critical init; show splash; lazy-load routes
4. First frameLayout + paint + rasterKeep 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

LeverTypical impact
Lazy-init Firebase / SDKs in parallel after first frame200–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 payloadsFrees main isolate for first frame
precacheImage for above-the-fold imagesNo empty boxes flashing in
Tree shaking + --obfuscate + --split-debug-infoSmaller binary → faster load
const constructors everywhereSkip rebuilds, reduce GC pressure
Remove unused fonts / icon glyphsSmaller 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

  1. 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.

  2. Where does flutter_native_splash fit 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.

  3. How do you instrument app-start performance in production? Firebase Performance Monitoring automatically captures _app_start traces. For finer control, add custom traces around your bootstrap stages (Firebase init, config fetch, first DB read). Sentry has similar appStart measurements. Aggregate over real-device populations — your dev machine is the fastest device any user will own.

  4. 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.