Harsh Mittal
Back to blog
2025-09-20ยท4 min read

Lightning-Fast Flutter: Cold Start in Just 2 Seconds ๐Ÿš€

FlutterPerformanceAlso on Medium

Lightning-Fast Flutter: Cold Start in Just 2 Seconds

A cold launch is really three phases โ€” engine and Dart VM init, your main() running and runApp() kicking off, then the first frame getting painted โ€” and startup feels slow when anything blocks that path: heavy work stuffed into main() or an early initState(), large synchronous JSON parsing, or a network call gating the first screen. The fix is less about clever tricks and more about discipline: keep main() down to runApp() and defer everything else (analytics, crash reporting, non-critical DB setup) until after the first frame renders, push CPU-heavy parsing onto a background isolate via compute(), and load features you don't need immediately via deferred imports so they're not part of the initial snapshot at all.

The rest is measurement, not guesswork โ€” flutter run --profile --trace-startup, the DevTools performance timeline, and Firebase Performance Monitoring's cold-vs-warm-start breakdown all make it obvious which phase is actually eating your time budget, rather than optimizing based on a hunch.

The Three Phases, in Detail

Engine and Dart VM init. This is Flutter's own startup work โ€” spinning up the Dart VM, loading the app's AOT-compiled snapshot, initializing the rendering engine. Almost none of it is under your control, but it's also almost never the actual bottleneck on modern hardware; it's typically a few hundred milliseconds and roughly constant regardless of app size.

main() running and runApp() kicking off. This is entirely under your control, and it's where most avoidable delay lives. Every synchronous line here โ€” a Hive box opening, a Firebase initializeApp() awaited before runApp(), a shared-preferences read โ€” pushes the first frame back by exactly that much.

First frame painted. Flutter has to build and lay out the entire initial widget tree before anything appears. A splash screen that's just a static image and a CircularProgressIndicator paints almost instantly; a home screen that fetches data before rendering anything paints only once that fetch resolves.

Defer Everything That Isn't the First Frame

The single highest-leverage change is usually the smallest one: call runApp() immediately and move everything else to run after.

void main() {
  runApp(const MyApp()); // first frame can paint immediately

  // deferred โ€” runs after the first frame, doesn't block it
  WidgetsBinding.instance.addPostFrameCallback((_) {
    FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
    AnalyticsService.init();
    _warmSecondaryCaches();
  });
}

Anything that isn't strictly required to render the very first screen โ€” crash reporting setup, analytics initialization, cache warming for screens the user hasn't navigated to yet โ€” belongs in that post-frame callback, not ahead of runApp().

Push Parsing Off the Main Thread

A splash screen that synchronously parses a large local JSON config, or decodes a big cached API response before showing anything, blocks the first frame for exactly as long as that parse takes. compute() moves it to a background isolate:

AppConfig _parseConfig(String raw) => AppConfig.fromJson(jsonDecode(raw));

Future<AppConfig> loadConfig() async {
  final raw = await rootBundle.loadString('assets/config.json');
  return compute(_parseConfig, raw);
}

For genuinely large payloads (tens of thousands of JSON entries, a bundled offline dataset), the isolate overhead is trivial compared to what it saves the main thread.

Deferred Loading for Features You Don't Need Immediately

Dart's deferred as import keeps a feature's code out of the initial app snapshot entirely, downloading and loading it only when first accessed:

import 'package:myapp/features/report_export.dart' deferred as report_export;

Future<void> openExportScreen() async {
  await report_export.loadLibrary();
  Navigator.push(context, MaterialPageRoute(builder: (_) => report_export.ExportScreen()));
}

This matters most for large, rarely-used features โ€” a PDF export module, an admin panel, a rarely-opened settings sub-screen โ€” where including them in the startup snapshot costs every user load time for a feature most of them never touch on first launch.

Measure Before You Optimize

Guessing which phase is slow wastes effort on the wrong fix. Three tools make it concrete instead:

  • flutter run --profile --trace-startup writes a startup timeline to build/start_up_info.json, breaking down exactly how long engine init, main(), and first-frame took.
  • DevTools' Performance tab, recorded from cold launch, shows the same breakdown visually and makes it obvious if a specific synchronous call is the culprit.
  • Firebase Performance Monitoring's cold-start vs. warm-start metrics show the real-world distribution across actual users and devices โ€” a fix that helps on a flagship test phone doesn't always translate to the low-end devices most of your users are actually on.

What "2 Seconds" Actually Requires

In practice, hitting a genuinely fast cold start is rarely one big change โ€” it's runApp() staying minimal, the splash screen staying visually simple (no network-dependent content), heavy parsing moved off the main thread, and anything non-critical deferred past the first frame. Each of these shaves tens to hundreds of milliseconds; stacked together on an app that previously did all of it eagerly, they're usually the difference between a 4-5 second cold start and one comfortably under 2.

Get new posts by email

No spam, no schedule โ€” just an email when a new post goes up.