Cold start vs warm start vs hot start — ¿cómo reducís de verdad el tiempo de arranque de una app?
Los tres tipos de arranque se saltan distinta cantidad de trabajo, así que antes de optimizar hay que saber cuál de los tres mediste en realidad.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Cold start vs warm start vs hot start — ¿cómo reducís de verdad el tiempo de arranque de una app?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Los tres tipos de arranque se definen por exactamente una cosa: cuánto tiene el sistema ya listo antes de que aparezca tu primer frame. Tener esta distinción precisa es lo que separa una respuesta sólida de un "hacelo más rápido de alguna forma".
Un cold start ocurre cuando ni tu proceso ni ninguna instancia de Activity existen —después de que arrancó el dispositivo, después de que el usuario (o el sistema, por presión de memoria) mató tu app, o en un primer lanzamiento genuino. El sistema tiene que crear un proceso nuevo desde el Zygote, construir tu objeto Application, inicializar cada ContentProvider declarado en el manifest, y recién ahí crear, iniciar y resumir la Activity de destino. Este es el camino lento, y es al que apunta la mayoría del trabajo de optimización de arranque.
Un warm start significa que el proceso ya está vivo, pero la instancia específica de Activity no —lo más común es que el sistema la haya recuperado de memoria mientras estaba en segundo plano, así que tiene que pasar de nuevo por onCreate(), luego onStart(), luego onResume(), con inflado completo de vistas incluido, pero se salta por completo la creación del proceso, Application.onCreate() y la inicialización de ContentProvider. También cubre el caso de una Activity completamente nueva lanzada dentro de un proceso que ya está corriendo.
Un hot start significa que tanto el proceso como la instancia de la Activity de destino siguen vivos en segundo plano —el usuario apretó Home y volvió, o navegó por recientes. El sistema solo necesita onRestart(), luego onStart(), luego onResume(): sin reinflar nada, sin trabajo de proceso, nada que reconstruir. Este es, por lejos, el camino más rápido, y acá tu trabajo consiste sobre todo en no convertirlo accidentalmente en un warm start haciendo trabajo pesado y re-disparable dentro de onResume().
Medir esto correctamente importa tanto como mejorarlo. adb shell am start -W <package>/<activity> reporta ThisTime, TotalTime y WaitTime para un solo lanzamiento, y Logcat imprime una línea Displayed en el momento en que se dibuja el primer frame —ese momento es el Time To Initial Display, o TTID. TTID no es lo mismo que "usable": una pantalla puede dibujar su toolbar y una lista vacía bastante antes de que lleguen los datos que existe para mostrar. Llamar a reportFullyDrawn() una vez que el contenido que el usuario realmente buscaba está en pantalla marca el Time To Full Display, o TTFD, y ese es el número que de verdad le importa tanto al vital de "arranque lento" de Play Console como a StartupTimingMetric de Macrobenchmark —optimizar el TTID ignorando el TTFD es optimizar el número que nadie experimenta.
Reducir el cold start es donde vive la mayor parte del trabajo real, porque es el camino más lento y por el que pasa cada instalación nueva. Application.onCreate() es el primer lugar para mirar: cada SDK que se inicializa ahí corre, por defecto, de forma síncrona en el hilo principal antes de que arranque siquiera tu primera Activity. La interfaz Initializer de androidx.startup reemplaza un montón de llamadas ad-hoc de inicialización de SDKs por inicializadores ordenados por dependencias que además colapsan lo que de otro modo sería un ContentProvider separado por librería —cada uno con su propio overhead de arranque— en uno solo. Más allá de eso: publicá un Baseline Profile, generado con BaselineProfileRule de la librería Macrobenchmark, que le indica al compilador de ART qué clases y métodos están "calientes" en un camino de arranque típico para compilarlos por adelantado en lugar de interpretarlos en frío en la primera ejecución —es uno de los cambios de mayor impacto por la cantidad de código que requiere. Mantené liviano tu grafo de inyección de dependencias, ya que un grafo de Dagger/Hilt construido con ansias arma cada binding singleton en el arranque, lo necesite o no la primera pantalla —preferí bindings lazy tipo Provider para todo lo que no esté en el camino crítico hacia el primer frame. Y auditá específicamente el I/O en el hilo principal durante el arranque —una lectura síncrona de preferencias o una carga de configuración respaldada en disco es exactamente el tipo de cosa que es invisible aislada y muy visible en conjunto en un cold launch.
// MAL: cada SDK bloquea el primer frame, en orden de registro,// en el hilo principal, lo necesite o no esta pantalla todavía.class MyApp : Application() {override fun onCreate() {super.onCreate()AnalyticsSdk.init(this) // hace una llamada de red por detrásCrashReporter.init(this) // prepara un buffer local en discoFeatureFlags.warmCache(this) // lectura bloqueante desde discoImageLoader.init(this) // arma un thread pool + caché en disco de entrada}}
// MEJOR: solo lo que debe poder observar un crash desde el frame uno// queda inline; el resto se vuelve un Initializer ordenado y testeable.class MyApp : Application() {override fun onCreate() {super.onCreate()CrashReporter.init(this)}}class AnalyticsInitializer : Initializer<AnalyticsSdk> {override fun create(context: Context): AnalyticsSdk = AnalyticsSdk.init(context)override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()}class ImageLoaderInitializer : Initializer<ImageLoader> {override fun create(context: Context): ImageLoader = ImageLoader.init(context)override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()}
// Marcando el TTFD explícitamente — el primer frame (TTID) puede ser// un toolbar y un shimmer; reportFullyDrawn() marca cuándo llega el contenido real.class HomeActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_home)viewModel.feed.observe(this) { items ->adapter.submitList(items)if (items.isNotEmpty()) reportFullyDrawn() // marca el TTFD, no solo el TTID}}}
Los warm y hot start tienen menos superficie para optimizar, pero no son gratis por defecto. Para el warm start, dejar que el sistema destruya y recree una Activity ante un cambio de configuración, reconstruyendo el estado a través de ViewModel más el estado guardado de la instancia, en general es la mejor decisión de ingeniería frente a pelear la recreación manejando manualmente los cambios de configuración —pero solo si ese camino de recreación es en sí mismo barato, lo que significa que la misma disciplina de arranque (un onCreate() liviano, sin I/O síncrono) aplica también acá, y con más razón. Para el hot start, el riesgo principal es código en onResume() que no es realmente idempotente ni barato —volver a consultar datos que no cambiaron, rehacer trabajo de layout, resuscribirse a listeners sin chequear si ya estaban suscriptos— porque un hot start vuelve a correr onResume() sin ningún límite de proceso o de dispatcher detrás del cual esconder el costo.
El encuadre senior que ata todo esto: separar la latencia percibida del trabajo real. La API SplashScreen de Android 12+ (con una librería de compatibilidad hacia atrás para versiones anteriores) muestra una splash dibujada por el sistema en el instante en que arranca el proceso, antes de que tu código haya corrido siquiera —no elimina ni un milisegundo de trabajo real, pero elimina la ventana en blanco que el usuario vería en su lugar, lo cual cambia de forma medible qué tan lento se siente el arranque. Hecho mal —dibujando tu propia Activity de splash con tema encima de la del sistema— obtenés un parpadeo visible de doble splash, un anti-patrón real que aparece más en las reseñas de la store que en los traces del profiler. Y tratá el tiempo de arranque como una métrica con gate en CI vía Macrobenchmark, no como algo que se mira a ojo de vez en cuando, porque las regresiones se cuelan de a un Application.onCreate() inocente por vez, y para cuando alguien nota que "se siente más lenta", nadie se acuerda cuál de los últimos veinte pull requests fue.
🧠 Cold, warm, hot: poné a prueba lo que entendiste
0/4 · 0/4 answered1. ¿Cuál describe mejor un warm start?
2. ¿Qué marca realmente reportFullyDrawn()?
3. ¿Por qué un Baseline Profile acelera el cold start?
4. ¿Cuál es el problema real de agregar una Activity de splash con tema propio encima de la splash screen del sistema en Android 12+?