Se traba es la descripción del usuario de una situación en la que una aplicación móvil funciona lenta e irregularmente: a veces responde con normalidad, a veces se congela repentinamente durante varios segundos. En el contexto técnico, “se traba” significa una combinación de rezagos y microcongelaciones causada por pausas frecuentes del GC, bloqueo del hilo principal por operaciones síncronas y estructuras de datos subóptimas. Según la Android Performance Benchmarking Guide, reducir el tiempo de respuesta de 300 ms a 100 ms aumenta la retención de usuarios en un 25%. Diagnosticar los bloqueos requiere una combinación de perfilado de CPU y Memoria con análisis de la frecuencia de recolección de basura.
Puntos clave
Se traba es un término informal que los usuarios emplean para describir un rendimiento subjetivamente lento de la aplicación. A diferencia de un lag, que se manifiesta como un retardo constante, los bloqueos consisten en congelaciones irregulares: la aplicación puede funcionar perfectamente durante varios segundos y luego “pensar” durante 1–3 segundos.
Desde la perspectiva del perfilado, bloquearse se manifiesta como una serie de fotogramas perdidos (jank) con retrasos pico superiores a 100 ms. En un gráfico de FPS, esto se ve como caídas pronunciadas: 60 → 20 → 55 → 10 fotogramas por segundo. A diferencia de un lag con FPS uniformemente bajo, los bloqueos tienen una variabilidad pronunciada.
Cuando una aplicación se traba, el usuario no comprende la lógica de las ralentizaciones: la pantalla puede desplazarse suavemente y luego detenerse repentinamente durante un segundo. Esto causa frustración y reduce la confianza en la aplicación. Según Google, el 53% de los usuarios abandonan un sitio o aplicación si la carga tarda más de 3 segundos.
La naturaleza intermitente de los bloqueos indica que el problema está causado por factores basados en eventos, no por una sobrecarga constante. Examinemos los escenarios típicos.
En Android, en el entorno ART, la recolección de basura detiene todos los hilos de la aplicación. Si el código crea muchos objetos temporales — por ejemplo, creando un nuevo String mediante concatenación en cada llamada a onBindViewHolder — el GC se ejecuta con más frecuencia. Una pausa puede durar 5–50 ms dependiendo del tamaño del heap y la generación de objetos. El usuario lo percibe como una “pensatividad” repentina.
Room en Android y Core Data en iOS admiten consultas asíncronas, pero los desarrolladores suelen llamar a getValue() o ejecutar consultas mediante runBlocking por simplicidad. Un SELECT pesado con joins en una tabla de 10 000 filas puede tardar 200–500 ms, bloqueando completamente la UI durante ese tiempo.
Cargar una imagen de cámara (12 MP, 4000x3000 px) sin escalado tarda hasta 200 ms en decodificar a Bitmap. Si las imágenes se cargan de forma asíncrona pero sin un grupo de hilos limitado, ejecutar 5–6 decodificaciones simultáneas puede sobrecargar la CPU, causando ralentizaciones migratorias.
Diagnosticar las ralentizaciones intermitentes es más difícil que diagnosticar los rezagos constantes porque el problema puede no reproducirse en cada ejecución. Se requiere la recopilación de estadísticas durante un período prolongado.
Android Studio Memory Profiler muestra no solo el uso de memoria sino también los eventos GC: frecuencia, tipo (Concurrent, Full), duración. Si el GC ocurre más de una vez cada 5 segundos en estado inactivo — es una señal de asignación excesiva. Tomar un heap dump en el momento del bloqueo revela qué objetos están ocupando memoria.
En iOS, use la plantilla Allocations en Instruments para rastrear la creación y liberación de objetos. Active Generaciones — permiten tomar instantáneas del heap entre acciones y ver qué objetos permanecen en memoria. Los objetos persistentes que no se liberan son una fuente de acumulación de memoria y posteriores pausas.
JankStats es una biblioteca de Android que recopila métricas de fotogramas perdidos en tiempo real. Asocia cada jank al escenario actual (por ejemplo, “desplazamiento de lista”, “apertura de pantalla”), lo que permite comprender qué acción específica desencadena el bloqueo.
Ejemplo de integración de JankStats para rastrear bloqueos en Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "Duration=${frameData.durationMs}ms")
}
}
}
}
Eliminar los bloqueos requiere un trabajo específico en cada causa. No existe una solución universal — se necesita un análisis de los perfiles de rendimiento específicos.
Si una lista contiene 1000+ elementos y todos se cargan a la vez — eso es un bloqueo garantizado. Paging 3 en Android y NSFetchedResultsController en iOS cargan datos porciones a medida que el usuario se desplaza. El usuario solo ve los primeros 10–20 elementos; el resto se cargan en segundo plano.
Room permite perfilar consultas mediante la Herramienta de Inspección en Android Studio: se ven el tiempo de ejecución, el número de filas devueltas y el plan de consulta. Agregar índices en las columnas WHERE y ORDER BY puede reducir el tiempo de consulta de 300 ms a 5 ms. En iOS, una verificación similar la realiza Core Data Profiler en Instruments.
Las sincronizaciones en segundo plano, descargas de archivos, procesamiento de datos — todo esto debe ejecutarse mediante WorkManager (Android) o Background Tasks (iOS). Si la sincronización se ejecuta en el hilo de UI, la aplicación se bloqueará durante la ejecución. WorkManager garantiza la ejecución en un hilo en segundo plano con conocimiento del estado de la batería y la red.
Ejemplo de sincronización en segundo plano mediante WorkManager en Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "Syncing data in background thread")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
Se pueden prevenir los bloqueos en la etapa de codificación siguiendo los principios de gestión eficiente de memoria y subprocesos.
Baseline Profiles son una lista de clases y métodos que Android compila anticipadamente (AOT) en lugar de JIT. Sin un perfil, cada nueva pantalla se compila al abrirse por primera vez, causando un retraso de 100–500 ms. Prepare un Baseline Profile para las pantallas clave y habilite la generación en Gradle mediante el complemento baseline-profile-gradle-plugin.
Una ruta crítica es el código que se ejecuta en cada fotograma: onBindViewHolder, draw, layoutSubviews. Evite crear objetos en estos métodos: use grupos de objetos, StringBuilder en lugar de concatenación, almacene en caché cadenas formateadas y formateadores. Cada asignación adicional acerca el próximo GC.
Agregue Macrobenchmark a su pipeline de CI con un escenario de desplazamiento de lista y apertura de pantalla. Establezca un umbral: el percentil 99 del tiempo de fotograma no debe exceder los 16 ms. Si se supera el umbral — la compilación se rechaza hasta la optimización.
Preguntas frecuentes
Un lag es un retardo constante (por ejemplo, 200 ms en cada toque). Bloquearse es intermitente: la aplicación funciona normalmente, luego repentinamente se ralentiza durante 1–3 segundos, luego vuelve a la normalidad. La causa son factores basados en eventos como pausas del GC o consultas síncronas a la base de datos.
Use Memory Profiler en Android Studio: la pestaña Memory muestra los eventos GC con duración. Para monitoreo en producción, integre Firebase Performance Monitoring con trazas personalizadas. En iOS, habilite Malloc Debug y marque las generaciones de asignaciones en Instruments.
Indirectamente — sí. Si la respuesta del servidor se retrasa y la UI espera síncronamente, la aplicación se congela. Si la solicitud es asíncrona pero el procesamiento de la respuesta se realiza en el hilo de UI — eso también causará bloqueos. La solución es el procesamiento asíncrono con corutinas e indicadores de progreso.
Cuando se usa incorrectamente, KMP puede generar objetos envoltorio excesivos para la interoperabilidad. En iOS, esto aumenta la frecuencia de asignaciones y, en consecuencia, las pausas de ARC. Use @ObjCName, optimice expect/actual y evite llamadas frecuentes al código compartido desde rutas críticas de la UI.
Aumentar el heap mediante android:largeHeap=”true” retrasa el GC pero no elimina la causa de las asignaciones. Cuando el GC finalmente se ejecuta, la pausa será más larga porque hay más objetos que recorrer. La solución es reducir el número de asignaciones, no expandir el heap.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también