| name | warm-and-hot-start |
| description | Optimize warm start (process alive, activity recreated) and hot start (app resumed to foreground) on iOS and Android. Covers process restoration, lazy singletons, and saved state. |
Warm and Hot Start
Instructions
Cold start gets all the attention, but warm and hot starts are what real users experience dozens of times a day. If these are slow, the app feels sluggish even when launch benchmarks look fine.
1. Definitions
- Cold start: process does not exist. OS forks, links, runs app init, draws first frame.
- Warm start: process exists but top Activity/ViewController was destroyed (memory pressure, config change). App must recreate UI state.
- Hot start: process and top view are alive; user tapped the icon or switched back. Ideally instant.
Targets (p50 device):
| Metric | iOS | Android |
|---|
| Warm TTID | ≤ 600ms | ≤ 800ms |
| Hot TTID | ≤ 250ms | ≤ 300ms |
2. Keep Hot Start Instant
Hot start should be a onResume/viewWillAppear call, nothing more. The killer pattern is forcing a full data refresh on foreground.
Bad:
override fun onResume() {
super.onResume()
viewModel.reload()
}
Better — refresh in the background, let the existing data paint immediately:
override fun onResume() {
super.onResume()
viewModel.refreshIfStale(maxAge = 60.seconds)
}
Stale-while-revalidate: render cached content, fetch in background, diff into the list when new data arrives. See caching-strategies skill.
3. Process Restoration on Android
When Android restores a process after a low-memory kill, the user sees a warm start. Handle it with saved state, not a redirect to the launcher activity.
class FeedViewModel(private val handle: SavedStateHandle) : ViewModel() {
var scrollOffset: Int
get() = handle["scroll"] ?: 0
set(value) { handle["scroll"] = value }
}
- Use
SavedStateHandle in every ViewModel. Persist scroll offset, filter state, selection.
- Do not call
finish() and restart from a launcher activity on restoration. That turns warm start into a cold-start-ish experience.
4. State Restoration on iOS
- Adopt
UISceneSession state restoration: implement stateRestorationActivity(for:) and application(_:configurationForConnecting:options:).
- For SwiftUI, use
@SceneStorage for per-scene values and @AppStorage for device-wide settings.
struct FeedView: View {
@SceneStorage("feed.scroll") var scrollOffset: Double = 0
@SceneStorage("feed.filter") var filter: String = ""
}
5. Lazy Singletons Everywhere
Singletons that initialize on first access make warm start cheap because the process already carries them. Singletons that initialize at Application.onCreate / AppDelegate make every warm start pay cold-start cost.
Kotlin:
object Analytics {
val client by lazy { AnalyticsClient(BuildConfig.API_KEY) }
}
Swift:
enum Analytics {
static let client: AnalyticsClient = {
AnalyticsClient(apiKey: Secrets.analyticsKey)
}()
}
Dart:
final analyticsProvider = Provider((ref) => AnalyticsClient());
TS (React Native):
let _client: AnalyticsClient | null = null;
export const getAnalytics = () => _client ??= new AnalyticsClient();
6. Resource Caches Survive Warm Start
Image caches (Coil, SDWebImage, cached_network_image, FastImage) persist on disk across process death. Keep their disk budget generous — re-fetching images on warm start is the #1 cause of visible flicker.
- Android Coil:
memoryCache(MemoryCache.Builder(ctx).maxSizePercent(0.25).build()), diskCache(DiskCache.Builder().directory(ctx.cacheDir.resolve("img")).maxSizeBytes(256L * 1024 * 1024).build()).
- iOS SDWebImage:
SDImageCache.shared.config.maxDiskSize = 256 * 1024 * 1024.
7. Measure
Android — use am start -W to print WaitTime (warm) and ThisTime (just the activity):
adb shell am start -W -n com.example.app/.MainActivity
iOS — Xcode Instruments App Launch template records "Resume" phases, which is hot start.
Checklist