Apps that take longer than 3 seconds to load lose up to 70% of first-time users. Yet most developers have no idea what their actual startup time is or how to measure it accurately.
Every developer has experienced this moment.
You tap an app… and nothing happens.
No UI. No feedback. Just a blank screen.
Three seconds later you’ve already assumed it’s frozen - and you close it.
Your users do exactly the same thing.
Startup performance isn’t a “nice-to-have metric.” It’s the first impression your product makes. If the first frame is slow, users don’t wait around to admire your architecture. They churn.
Even worse: Google factors startup performance heavily into Play Store visibility, and Apple rejects apps that exceed their launch time thresholds.
If your app is slow, you're losing users and discoverability.
This guide will teach you exactly how to measure startup performance on Android, iOS, and Flutter, using the same tools and techniques top apps use to maintain sub-2-second launch times.
First, understand what “startup” really means
Not every launch is the same. Sometimes the system has to create your process from scratch. Other times your app is already sitting in memory. These scenarios behave very differently, which is why measuring “launch time” without context is misleading.
A cold start - when the process doesn’t exist and the OS has to load your code, initialize the framework, and build your first screen is the slowest and most important case. It’s also the one users see the first time they open your app.
Warm and hot starts are faster, but they don’t matter if users abandon during the very first launch.
In practice, most mobile teams aim for a cold start under two seconds on mid-range devices. That’s not an official rule - just a threshold that consistently feels instant to humans.
Types of App Startup

Understanding startup types is critical because each has different performance characteristics and thresholds.
| Type | What Happens | When It Occurs | Target Time |
|---|---|---|---|
| Cold Start | App process created from scratch. System loads code, initializes framework, creates main activity. | First launch after install, reboot, or process killed by system | < 2 seconds |
| Warm Start | Process exists but activity needs recreation. Faster than cold start. | Returning after extended background time | < 1 second |
| Hot Start | Activity brought to foreground. Minimal work required. | Switching back to an already-running app | < 500 ms |
Why cold start matters most: It's the first impression your app makes. Users won't wait around to see if warm starts are faster.
Why Startup Performance Matters
Startup time isn't just a vanity metric. It directly impacts your bottom line:
| Impact | Statistic |
|---|---|
| User Abandonment | 88% of users will abandon apps due to slow performance |
| Conversion Drop | Every 1-second delay causes a 7% drop in conversions |
| Uninstall Rate | Apps exceeding 6 seconds see a 33% higher uninstall rate |
| App Store Ranking | Google tracks startup times in Android Vitals and penalizes slow apps |
The data is clear: fast apps win. But before you can optimize, you need to measure.
Platform-Specific Thresholds
Google and Apple have clear expectations. Exceeding these hurts your app store ranking:
| Platform | Cold Start Threshold | Warm Start Threshold | Hot Start Threshold |
|---|---|---|---|
| Android (Google Vitals) | ≥ 5 seconds = "excessive" | ≥ 2 seconds = "excessive" | ≥ 1.5 seconds = "excessive" |
| iOS (Apple Guidelines) | First frame in < 400 ms (recommended) | < 1 second | < 500 ms |
| Flutter | < 2 seconds (recommended) | < 1 second | < 500 ms |
Sources: Android Developers - App Startup Time, Apple Developer - Improving App Responsiveness
How to Measure Startup Time
Key Metrics to Track
Before diving into tools, understand what you're measuring:
| Metric | Definition | Why It Matters |
|---|---|---|
| TTID (Time to Initial Display) | Time until first frame is rendered on screen | User perceives app as "responsive" |
| TTFD (Time to Full Display) | Time until app is fully interactive with all content loaded | User can actually use the app |
| Time to First Frame (TTFF) | Flutter-specific: time from engine start to first frame | Core metric for Flutter apps |
Rule of thumb: TTID should be < 2 seconds. TTFD should be < 4 seconds for content-heavy apps.
Measuring startup on Android

Method 1: Using Android Studio Logcat (Quick Check)
The simplest way to check startup time is via Logcat: If you build for Android, everything starts with Android Studio and the tools Google already ships.
The quickest sanity check is surprisingly simple. Launch your app and run:
# Run this in terminal while launching your app
adb logcat -d | grep "Displayed"/Expected output:
Displayed com.yourapp/.MainActivity: +1s234msThis shows TTID (time to initial display). The +1s234ms means the first frame appeared 1.234 seconds after the app started.
Method 2: Android Vitals (Production Data)
For real-world performance data across thousands of users:
- Go to Google Play Console
- Navigate to Quality > Android Vitals > App startup time
- Review cold, warm, and hot start distributions
What to look for:
- 90th percentile cold start time (this is what slow devices experience)
- Percentage of sessions exceeding thresholds
- Trends over app versions
Once you need deeper insight - like figuring out exactly what’s blocking the main thread - switch to Perfetto. It records a full system trace so you can literally see class loading, content providers, and expensive initializers slowing things down.
Method 3: Perfetto Trace (Deep Analysis)

For identifying exactly what is slow:
# Start a perfetto trace
adb shell perfetto \\
-c - --txt \\
-o /data/misc/perfetto-traces/trace \\
<<EOF
buffers: {
size_kb: 8960
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.process_stats"
target_buffer: 0
process_stats_config {
scan_all_processes_on_start: true
}
}
}
duration_ms: 10000
EOF
# Launch your app
adb shell am start -W -n com.yourapp/.MainActivity
# Pull the trace
adb pull /data/misc/perfetto-traces/trace trace.perfettoOpen the trace at ui.perfetto.dev to visualize exactly what's happening during startup.
Method 4: Programmatic Measurement
Add this to your MainActivity to log startup time:
// MainActivity.kt
class MainActivity:AppCompatActivity(){
overridefunonCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Report when app is fully drawn
window.decorView.post{
val startupTime= System.currentTimeMillis()- processStartTime
Log.d("StartupTime","TTID:${startupTime}ms")
// For Android 10+
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.Q){
reportFullyDrawn()
}
}
}
companionobject{
privateval processStartTime= System.currentTimeMillis()
}
}Making Android startup faster
Once you measure, patterns appear quickly. The biggest mistake most apps make is doing too much work before the first frame.
Analytics setup, Remote config fetches, Database migrations, Crash SDKs. All blocking the main thread while the user stares at a blank screen.
The fix is simple: show UI first, initialize later.
Modern apps often use Jetpack App Startup to control which components load eagerly and which can be deferred. Moving just a few initializers can cut hundreds of milliseconds instantly.
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.init(context)
}
override fun dependencies() = emptyList<Class<out Initializer<*>>>
()
}Avoid main-thread work
Common mistakes:
- disk IO
- JSON parsing
- DB setup
- Network calls
Anything >4ms risks frame drops.
You’ll also see big wins from reducing APK size, enabling R8, and generating Baseline Profiles so Android can precompile hot paths ahead of time.
Reduce dex/class loading
- remove unused libraries
- enable R8
- split features
Smaller app = faster cold start.
Defer heavy initialization
Move non-critical work out of Application.onCreate().
Bad:
Analytics.init()
CrashReporting.init()
Database.migrate()Good:
Handler(Looper.getMainLooper()).post {
Analytics.init()
CrashReporting.init()
}Why use Handler here?
On Android, the main (UI) thread processes tasks one by one during startup. If you initialize analytics, crash reporting, or databases inside onCreate(), they run before the first frame draws, which directly delays launch.
Handler(Looper.getMainLooper()).post { ... } simply says:
“Run this after the current startup work finishes.”
So the sequence becomes:
- draw first frame
- show UI to user
- then initialize SDKs
The work still happens - just after the screen is visible, which improves perceived startup time.
Important: this doesn’t create a background thread. It only defers small, non-critical tasks. Heavy work should still move to IO/background threads.
Cold start performance is mostly about one principle: do less work before drawing the first frame.
Measuring startup on iOS

On iOS, measurement lives inside Xcode.
Method 1: Xcode Time Profiler
- Open your project in Xcode
- Go to Product > Profile (or ⌘+I)
- Select Time Profiler
- Click Record, then launch your app
- Look for the UIApplicationMain to viewDidAppear span
Method 2: Adding Pre-Main Time Logging
Add this environment variable in your scheme to see pre-main time:
- Product > Scheme > Edit Scheme
- Under Run > Arguments > Environment Variables, add:
- DYLD_PRINT_STATISTICS = 1
Output example:
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 800.0 milliseconds (66.6%)
rebase/binding time: 100.0 milliseconds (8.3%)
ObjC setup time: 50.0 milliseconds (4.1%)
initializer time: 250.0 milliseconds (20.8%)Method 3: Programmatic Measurement
// AppDelegate.swift or App.swift
importFoundation
@main
structMyApp:App{
init(){
// Mark launch start time (do this as early as possible)
LaunchTimeTracker.shared.markStart()
}
var body:someScene{
WindowGroup{
ContentView()
.onAppear{
LaunchTimeTracker.shared.markEnd()
}
}
}
}
classLaunchTimeTracker{
staticlet shared=LaunchTimeTracker()
privatevar startTime:CFAbsoluteTime=0
funcmarkStart(){
startTime=CFAbsoluteTimeGetCurrent()
}
funcmarkEnd(){
let launchTime=(CFAbsoluteTimeGetCurrent()- startTime)*1000
print("📱 App launch time:\\(String(format:"%.2f", launchTime))ms")
// Send to analytics
Analytics.track("app_launch_time", properties:["duration_ms": launchTime])
}
}Method 4: MetricKit
For iOS 13+, use MetricKit to collect startup data from real users:
importMetricKit
classMetricManager:NSObject,MXMetricManagerSubscriber{
funcdidReceive(_ payloads:[MXMetricPayload]){
for payloadin payloads{
iflet launchMetrics= payload.applicationLaunchMetrics{
let coldLaunch= launchMetrics.histogrammedTimeToFirstDraw
.bucketEnumerator.allObjects
print("Cold launch histogram:\\(coldLaunch)")
}
}
}
}Making iOS startup faster
The same principle applies here: do less work up front.
Heavy logic inside AppDelegate, global singletons that initialize on load, or dozens of dynamic frameworks all add invisible delay before the first frame appears.
Cutting dependencies, lazy-loading features, and rendering a lightweight first screen before loading content often reduces startup time more than any micro-optimization ever will.
Avoid work in AppDelegate / init
Bad:
Analytics.start()
Database.setup()
RemoteConfig.fetch()Better:
DispatchQueue.main.async {
Analytics.start()
}or defer until first screen appears.
Why use DispatchQueue.main.async here?
On iOS, everything during launch runs on the main thread. If you start analytics, databases, or SDK setup inside AppDelegate or didFinishLaunching, that work blocks the thread before the first screen appears, increasing startup time.
DispatchQueue.main.async {
Analytics.start()
}This simply defers the work to the next run loop cycle.
So the order becomes:
- render first screen
- UI becomes visible
- then analytics initializes
Nothing magical - you’re just letting the first frame render first.
Important: this still runs on the main thread, so only defer lightweight setup. Heavy work should move to background queues.
Remember: users don’t care if data loads 200ms later. They care if nothing appears at all.
Flutter: Step-by-Step Measurement

Method 1: Flutter DevTools Timeline
- Run your app in profile mode:
flutter run --profile- Open Flutter DevTools (the URL appears in terminal)
- Go to the Timeline tab
- Look for these key spans:
- Framework initialization
- First useful frame



