Startup Time Budgets That Actually Work
The elusive “fast startup” is a perennial goal for mobile app developers. We all *want* our apps to launch instantaneously, but translating that desire into concrete, actionable metrics and enforcing
Startup Time Budgets: From Wishful Thinking to CI Enforcement
The elusive “fast startup” is a perennial goal for mobile app developers. We all *want* our apps to launch instantaneously, but translating that desire into concrete, actionable metrics and enforcing them within a CI/CD pipeline is where the real challenge lies. Simply saying "startup should be fast" is an engineering cliché. What does "fast" actually mean for a complex e-commerce app versus a utility tool? How do we measure it reliably across different device states (cold, warm, hot starts)? And critically, how do we prevent regressions from creeping in, ensuring that new features don't introduce performance bottlenecks that alienate users? This article will delve into establishing realistic startup time budgets, robust measurement techniques, and a practical enforcement pattern for your CI pipeline, moving beyond vague aspirations to tangible engineering discipline.
Defining "Fast": Contextualizing Startup Time Across App Categories
The first hurdle is acknowledging that a one-size-fits-all startup time target is a fallacy. A banking application, laden with security checks, data synchronization, and complex UI rendering, will inherently have a different baseline than a simple note-taking app. We need to establish meaningful, category-specific benchmarks.
E-commerce & Social Media Apps: These are high-engagement applications where immediacy is paramount. Users expect to browse products or see their feed within seconds.
- Cold Start Target: 1.5 - 3 seconds. This includes the time from tapping the icon to the app being interactive and displaying primary content. This is the most critical metric as it represents the first impression.
- Warm Start Target: 0.5 - 1.5 seconds. After initial launch, users might close and reopen the app. This warm start should be significantly faster, as much of the app's state and resources are already in memory.
- Hot Start Target: < 0.5 seconds. This is the ideal scenario where the app is already in the background, and returning to it should be nearly instantaneous.
Productivity & Utility Apps: These apps, while important, may tolerate slightly longer startup times if the core functionality is robust and reliable.
- Cold Start Target: 2.5 - 5 seconds. Users might be more forgiving if the app provides significant value or performs a complex initialization.
- Warm Start Target: 1 - 2.5 seconds.
- Hot Start Target: < 1 second.
Games & Media Streaming Apps: These often involve significant asset loading, decompression, and engine initialization.
- Cold Start Target: 5 - 10 seconds. While still a concern, users often anticipate a longer wait for rich experiences. However, showing a loading indicator and progress is crucial.
- Warm Start Target: 2 - 5 seconds.
- Hot Start Target: 1 - 3 seconds.
Key Takeaway: These are not arbitrary numbers. They are derived from user behavior studies, competitive analysis, and empirical data. For instance, studies by Nielsen Norman Group consistently show that users perceive a response time of 0.1 seconds as instantaneous, 1 second as seamless, and 10 seconds as a significant interruption. For mobile apps, where impatience is amplified by network variability and device resource constraints, these thresholds are even tighter.
Measuring Startup Time: The Nuances of Cold, Warm, and Hot Starts
Reliable measurement is the bedrock of any performance budget. Simply timing from the moment you tap an icon to the first frame appearing on screen is insufficient. We must differentiate between the various states an app can be in upon launch.
#### Cold Start: The First Impression
A cold start occurs when the application process is not currently running. This happens when the user launches the app for the first time after installation, after a device reboot, or after the operating system has killed the app due to memory pressure.
What's Measured:
- Application Initialization: Time taken by the OS to create the application process.
- Application
onCreate(): Execution of theActivity.onCreate()method (Android) orapplication(_:didFinishLaunchingWithOptions:)(iOS). - Framework Initialization: Loading of libraries, dependency injection frameworks (e.g., Dagger Hilt, Koin), and SDKs.
- Data Loading: Fetching initial data from local storage, SharedPreferences, or making an initial network request.
- UI Rendering: The time until the first frame is drawn and the app becomes interactive.
Measurement Techniques:
- Android Studio Profiler: The built-in profiler offers a detailed breakdown of startup times.
- Procedure:
- Connect your Android device or start an emulator.
- Open your project in Android Studio.
- Navigate to
Run>Profile 'app'. - Select your app's module and click
OK. - Once the app launches on the device/emulator, observe the "Startup" section in the Profiler. It will show CPU usage and method traces, allowing you to pinpoint bottlenecks.
- To specifically measure cold start, force-stop the app from the device's "Apps" settings before profiling.
adb shell am startwith timing: This command-line tool allows programmatic measurement of cold starts.
- Command:
adb shell am start -W -n com.your.package.name/.YourMainActivity
-W: Waits for the activity to be launched and drawn.-n: Specifies the component name (package name and activity class).TotalTime: 1234ms. This is the total time from the am start command being issued to the activity being fully drawn.- Custom Instrumentation (Android): For more granular control, you can use custom
Instrumentationtests.
- Example Snippet (Kotlin):
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import android.content.Intent
import android.os.SystemClock
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.UiDevice
@RunWith(AndroidJUnit4::class)
class ColdStartTest {
@Test
fun measureColdStartup() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val targetContext = instrumentation.targetContext
val packageName = targetContext.packageName
val activityName = "$packageName/.MainActivity" // Replace with your launcher activity
// Ensure the app is not in memory
val pm = targetContext.packageManager
pm.getLaunchIntentForPackage(packageName)?.let { intent ->
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
targetContext.startActivity(intent)
}
// Wait for the app to be killed if it was running
Thread.sleep(1000) // Give OS time to kill, adjust if needed
val startTime = SystemClock.elapsedRealtime()
val intent = instrumentation.targetContext.packageManager.getLaunchIntentForPackage(packageName)
intent?.let {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
instrumentation.targetContext.startActivity(it)
}
// Wait until the main activity is displayed and interactive
// This requires knowing a unique element on your main screen
val uiDevice = UiDevice.getInstance(instrumentation)
val uniqueElement = By.res("$packageName:id/some_unique_view_id") // Replace with an actual ID
uiDevice.wait(Until.hasObject(uniqueElement), 10000) // Timeout of 10 seconds
val endTime = SystemClock.elapsedRealtime()
val startupTime = endTime - startTime
println("Cold Startup Time: $startupTime ms")
// Assertions can be added here to check against budget
// assertThat(startupTime, lessThanOrEqualTo(3000L))
}
}
#### Warm Start: The Quick Return
A warm start occurs when the application process is already running in the background. This happens when the user navigates away from the app and then returns to it shortly after. The OS doesn't need to recreate the process, but the activity might still be recreated.
What's Measured:
- Activity Recreation: If the activity was destroyed due to memory constraints, it will be recreated.
- UI State Restoration: Time taken to restore the UI state from
onSaveInstanceState()or ViewModel. - Background Task Resumption: If background tasks were paused, they might resume.
- Minimal Data Refresh: Potentially a quick check for updated data.
Measurement Techniques:
- Android Studio Profiler: Similar to cold start, you can profile warm starts.
- Procedure:
- Launch your app normally.
- Navigate to the home screen (or another app).
- Immediately re-open your app.
- Use the Profiler to observe the startup sequence. The process will already be running, so the initial process creation time will be absent.
adb shell am startwith timing: The-Wflag still works here, but the underlying process is already alive.
- Procedure:
- Launch your app.
- Press the home button.
- Run the
adb shell am start -W ...command again. The time reported will reflect the warm start.
- Custom Instrumentation (Android): Modify the previous instrumentation test.
- Procedure:
- Launch the app normally.
- Press the home button.
- Run the instrumentation test. The test will then launch the app again, simulating a warm start.
#### Hot Start: The Instantaneous Return
A hot start is the fastest scenario. The application process is running, and the activity is in memory and not destroyed. This is what happens when the user quickly switches back to an app they were just using.
What's Measured:
- Activity Resumption: Primarily the
onResume()lifecycle callback and potentiallyonNewIntent(). - UI State Refresh: Minimal UI updates if any.
Measurement Techniques:
- Android Studio Profiler: Profile the app as described for warm starts, but focus on the very rapid return. The profiler will show minimal work being done.
- Manual Observation & Logging: For hot starts, precise programmatic measurement can be tricky due to the speed. Often, developers rely on:
- In-app Timers: Logging timestamps within
onResume()and comparing them to a previousonPause()timestamp (though this measures foregrounding, not full app startup). - System Traces: Using tools like Perfetto to capture detailed system-level events.
#### iOS Considerations
For iOS applications, the concepts are similar, but the APIs and measurement tools differ.
- Cold Start: Time from
-[UIApplicationDelegate application:didFinishLaunchingWithOptions:]to the first screen being rendered. - Warm Start: Time from
-[UIApplication applicationWillEnterForeground:]and-[UIViewController viewWillAppear:]to the screen being rendered. - Hot Start: Primarily
-[UIViewController viewDidAppear:]and the app being fully interactive.
Measurement Tools (iOS):
- Xcode Instruments: The Time Profiler and Launch screen template are invaluable.
- Procedure:
- Open your project in Xcode.
- Go to
Product>Profile. - Choose the
Launchinstrument template. - Run your app. Instruments will automatically measure launch times.
- To measure cold starts, ensure the app is fully terminated from the device's multitasking view. For warm/hot starts, switch to another app and back.
os_signpostAPI: For custom, in-app timing of specific events.
- Example (Swift):
import os
let subsystem = Bundle.main.bundleIdentifier!
let oslog = OSLog(subsystem: subsystem, category: "Startup")
// In your AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let startupTrace = os_trace_begin_interval(oslog, "Cold Startup")
// ... your app initialization code ...
os_trace_end_interval(startupTrace)
return true
}
// In your ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let startupTrace = os_trace_begin_interval(oslog, "Warm/Hot Start")
// ... UI rendering completion ...
os_trace_end_interval(startupTrace)
}
#### The Role of Autonomous QA Platforms
Manually running these measurements across various devices and OS versions is tedious and error-prone. This is where autonomous QA platforms like SUSA come into play. By automating the exploration of an application, SUSA can reliably trigger launches in different states (simulating cold, warm, and hot starts by clearing app data, backgrounding, and returning) and capture performance metrics.
- SUSA's Approach: SUSA can be configured to perform a "startup test" as part of its exploration. It launches the app, waits for a defined period (or until a specific UI element appears), and records the time. By clearing app data before each run, it reliably simulates cold starts. By backgrounding and re-foregrounding, it simulates warm/hot starts. The platform aggregates these metrics across multiple runs and device configurations, providing a comprehensive performance overview. This automated approach ensures consistency and allows for frequent performance checks.
Implementing Startup Time Budgets in CI/CD
A performance budget is only effective if it's actively managed and enforced. Integrating these budgets into your CI/CD pipeline is crucial for preventing regressions.
#### The Budget Enforcement Pattern
The core idea is to establish a threshold for each startup type and fail the build if these thresholds are exceeded. This acts as a gatekeeper, preventing code that degrades startup performance from reaching production.
Key Components:
- Performance Testing Script: A script that runs your chosen measurement technique (e.g.,
adb shell am start, custom instrumentation, or an automated tool like SUSA's CLI). - Configuration File: Stores the defined budgets for different app categories or even specific features.
- CI/CD Job: Orchestrates the execution of the performance testing script and applies the budget checks.
- Reporting Mechanism: Provides clear feedback on whether the budget was met or violated, including specific metrics.
#### Example: CI Job with adb shell am start and JUnit XML Reporting
Let's outline a hypothetical CI job using GitHub Actions and adb shell am start for Android.
1. Performance Testing Script (scripts/measure_startup.sh)
#!/bin/bash
APP_PACKAGE="com.your.package.name"
LAUNCHER_ACTIVITY=".MainActivity"
EMULATOR_SERIAL="emulator-5554" # Or your device serial
# --- Configuration ---
COLD_START_BUDGET_MS=3000 # 3 seconds
WARM_START_BUDGET_MS=1500 # 1.5 seconds
# HOT_START_BUDGET_MS=500 # Hot start is harder to measure reliably with am start, often inferred
# --- Helper function to measure startup ---
measure_startup() {
local start_command="$1"
local description="$2"
local budget_ms="$3"
local output_file="$4"
echo "Measuring $description..."
# Execute the command and capture output
local result=$(adb -s $EMULATOR_SERIAL shell am start -W -n $APP_PACKAGE/$LAUNCHER_ACTIVITY)
local total_time=$(echo "$result" | grep "TotalTime:" | cut -d' ' -f2)
if [ -z "$total_time" ]; then
echo "ERROR: Could not capture $description time."
echo "$result" # Print raw output for debugging
return 1 # Indicate failure
fi
echo "$description Time: $total_time ms"
# Compare against budget
if [ "$total_time" -gt "$budget_ms" ]; then
echo "FAIL: $description exceeded budget ($total_time ms > $budget_ms ms)"
# Generate JUnit XML for reporting
cat <<EOF > $output_file
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="Startup Performance" tests="1" failures="1" errors="0" time="$total_time">
<testcase name="$description" classname="StartupPerformance" time="$total_time">
<failure message="Startup time exceeded budget ($total_time ms > $budget_ms ms)" type="performance">
Startup time exceeded budget ($total_time ms > $budget_ms ms). Budget: $budget_ms ms.
</failure>
</testcase>
</testsuite>
</testsuites>
EOF
return 1 # Indicate failure
else
echo "PASS: $description within budget ($total_time ms <= $budget_ms ms)"
# Generate JUnit XML for reporting
cat <<EOF > $output_file
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="Startup Performance" tests="1" failures="0" errors="0" time="$total_time">
<testcase name="$description" classname="StartupPerformance" time="$total_time">
</testcase>
</testsuite>
</testsuites>
EOF
return 0 # Indicate success
fi
}
# --- Main Execution ---
# Ensure ADB is available and device is connected
if ! adb devices | grep -q $EMULATOR_SERIAL; then
echo "ERROR: Device/Emulator $EMULATOR_SERIAL not found."
exit 1
fi
# Force stop the app to ensure a cold start measurement
echo "Force stopping app to ensure cold start..."
adb -s $EMULATOR_SERIAL shell am force-stop $APP_PACKAGE
# Measure Cold Start
measure_startup "Cold Start" $COLD_START_BUDGET_MS "cold_start_report.xml"
COLD_START_STATUS=$?
# For warm start, launch the app, go to background, then measure
echo "Launching app for warm start measurement..."
adb -s $EMULATOR_SERIAL shell am start -n $APP_PACKAGE/$LAUNCHER_ACTIVITY
sleep 2 # Give app time to initialize and go to background
adb -s $EMULATOR_SERIAL shell input keyevent KEYCODE_HOME
sleep 1 # Ensure app is in background
echo "Measuring Warm Start..."
measure_startup "Warm Start" $WARM_START_BUDGET_MS "warm_start_report.xml"
WARM_START_STATUS=$?
# Combine reports (optional, or use separate artifacts)
# For simplicity, we'll exit with a non-zero status if any test failed.
if [ $COLD_START_STATUS -ne 0 ] || [ $WARM_START_STATUS -ne 0 ]; then
echo "Startup performance tests failed."
exit 1
else
echo "All startup performance tests passed."
exit 0
fi
2. GitHub Actions Workflow (.github/workflows/performance.yml)
name: Performance Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
cache: 'gradle'
- name: Grant execute permission for script
run: chmod +x scripts/measure_startup.sh
- name: Start Android Emulator
uses: reactivecircus/android-emulator-runner@v1
with:
api-level: 30
arch: x86_64
profile: pixel # You might need to adjust this based on your emulator needs
disable-animations: true # Crucial for reliable timing
- name: Run Startup Performance Tests
id: startup_tests
run: ./scripts/measure_startup.sh
env:
APP_PACKAGE: "com.your.package.name" # Replace with your app's package name
LAUNCHER_ACTIVITY: ".MainActivity" # Replace with your launcher activity
EMULATOR_SERIAL: "emulator-5554" # Default for reactivecircus/android-emulator-runner
- name: Upload JUnit reports
uses: actions/upload-artifact@v3
if: always() # Upload reports even if tests fail
with:
name: startup-performance-reports
path: |
cold_start_report.xml
warm_start_report.xml
# This step will fail the build if the script exits with a non-zero status
# The 'startup_tests' step above already handles the exit code.
# You can add explicit checks here if needed, but the script's exit code is primary.
Explanation:
-
measure_startup.sh: - Defines budgets (in milliseconds).
- Uses
adb shell am start -Wto measure cold and warm starts. - Crucially, it force-stops the app before measuring cold start and backgrounds it before measuring warm start.
- Generates JUnit XML reports for each measurement, indicating success or failure with a descriptive message. This format is widely understood by CI/CD platforms.
- Exits with
0for success and1for failure.
-
.github/workflows/performance.yml: - Sets up the environment (Java, Android SDK via
reactivecircus/android-emulator-runner). -
disable-animations: trueis critical for consistent timing on emulators. - Executes the shell script. The
id: startup_testsallows us to reference its exit code later if needed, but the script's direct exit code is what determines job success. - Uploads the generated JUnit XML reports as artifacts, allowing inspection even if the build fails.
Refinements and Considerations:
- Hot Start Measurement:
adb shell am startis less ideal for hot starts. For true hot start measurement, you'd need more sophisticated instrumentation or rely on the profiling capabilities of tools that can precisely track activity lifecycle events and UI rendering completion. - Device Variability: Running this on a single emulator might not reflect real-world performance. Consider using a matrix of devices or cloud-based device farms.
- Network Conditions: Startup times can be heavily influenced by network. Your CI job might need to simulate different network conditions or run tests when network latency is predictable (e.g., during off-peak hours).
- Application State: Ensure the app is in a consistent state before each measurement. This might involve clearing cache, deleting user data, or resetting specific preferences.
- SUSA Integration: Instead of a custom script, you could use SUSA's CLI for performance testing. SUSA can be configured to perform specific performance checks, including startup time, and output results in a machine-readable format (like JSON or JUnit XML) that your CI pipeline can consume. This abstracts away the complexities of device management and measurement techniques.
- Example SUSA CLI Integration (Conceptual):
- name: Run SUSA Performance Tests
run: |
susa perf --app-path path/to/your/app.apk --test-type startup --budget-ms 3000 --output-format junit > susa_startup_report.xml
env:
SUSA_API_KEY: ${{ secrets.SUSA_API_KEY }}
This would instruct SUSA to test startup performance, upload the APK, enforce a 3000ms budget, and output JUnit XML.
#### Beyond Basic Checks: Advanced Budget Enforcement
- Historical Trend Analysis: Don't just fail on a single violation. Track startup times over time. A gradual increase might indicate a subtle but persistent issue that's easy to miss with simple threshold checks. Tools like Prometheus and Grafana can be used to visualize these trends.
- Feature Flagged Budgets: For new features that are known to impact startup, you might temporarily relax the budget or have a separate, more lenient budget associated with that feature flag.
- Staged Rollouts: Use CI/CD to gate releases. A build might pass performance tests on a staging environment but then undergo further performance monitoring in a canary release before a full rollout.
- User-Centric Metrics: While technical metrics are vital, consider correlating them with user-facing impact. Tools that track ANRs (Application Not Responding) or crash rates during startup can provide a more direct link to user experience. SUSA, for example, can identify ANRs that occur during startup, directly linking a performance issue to a user-impacting event.
The Role of Autonomous QA in Maintaining Startup Performance
The challenge with performance budgets isn't just setting them; it's maintaining them consistently across a rapidly evolving codebase. This is where autonomous QA platforms shine.
- Consistent Measurement: Autonomous platforms like SUSA execute tests repeatedly across defined device configurations. This eliminates human error and ensures that measurements are taken under comparable conditions.
- Exploratory Performance Testing: Beyond predefined scripts, SUSA's AI-driven exploration can uncover unexpected performance bottlenecks. For example, its personas might navigate through a complex purchase flow, and the platform can monitor the time taken for each step, identifying slow transitions or rendering issues that manual tests might miss.
- Automated Script Generation: A significant advantage is SUSA's ability to auto-generate regression scripts (e.g., Appium, Playwright) from its exploration runs. If a startup performance issue is detected during exploration, SUSA can automatically create a script to specifically re-test that scenario in future runs, ensuring the regression is caught quickly.
- Cross-Session Learning: As SUSA interacts with your app over time, it learns its behavior. This learning can be applied to refine performance tests, identify more subtle anomalies, and predict potential performance degradation based on code changes. For instance, if SUSA observes that a particular screen consistently takes longer to load after specific code commits, it can flag this as a potential performance risk.
- Comprehensive Issue Detection: SUSA doesn't just look at raw timing. It can identify crashes and ANRs that occur during startup, providing direct evidence of critical performance failures. It also checks for accessibility violations (WCAG 2.1 AA) and security vulnerabilities (OWASP Mobile Top 10), which can sometimes be exacerbated by or contribute to slow startup.
By integrating autonomous QA into the development lifecycle, teams can shift performance testing from a reactive, post-development chore to a proactive, continuous process. This allows for the early detection and remediation of startup performance issues, ensuring that the "fast startup" goal remains a reality, not just an aspiration.
Conclusion: From Reactive Fixes to Proactive Performance Engineering
Establishing and enforcing startup time budgets is not a one-time task; it's a commitment to performance engineering. It requires a clear understanding of user expectations, robust measurement methodologies, and a disciplined approach to integrating these metrics into your development workflow. By moving beyond vague goals and implementing concrete, automated checks within your CI/CD pipeline, you can transform startup performance from a source of technical debt into a competitive advantage. The key is to make performance a first-class citizen, measured, budgeted, and actively defended at every stage of development.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free