Rallentamento è la descrizione da parte dell’utente di una situazione in cui un’app mobile funziona lentamente e in modo irregolare: a volte risponde normalmente, a volte si blocca improvvisamente per alcuni secondi. Nel contesto tecnico, “rallentamento” significa una combinazione di lag e micro-blocchi causati da frequenti pause GC, blocco del thread principale da operazioni sincrone e strutture dati non ottimali. Secondo la Android Performance Benchmarking Guide, ridurre il tempo di risposta da 300 ms a 100 ms aumenta la fidelizzazione degli utenti del 25%. Diagnosticare i rallentamenti richiede una combinazione di profilazione CPU e Memoria con analisi della frequenza di garbage collection.
Punti chiave
Rallentamento è un termine informale che gli utenti usano per descrivere prestazioni dell’app soggettivamente lente. A differenza di un lag, che si manifesta come un ritardo costante, il rallentamento consiste in blocchi irregolari: l’app può funzionare perfettamente per diversi secondi e poi “pensare” per 1–3 secondi.
Dal punto di vista della profilazione, il rallentamento si manifesta come una serie di frame saltati (jank) con picchi di ritardo superiori a 100 ms. In un grafico FPS, questo appare come cali improvvisi: 60 → 20 → 55 → 10 frame al secondo. A differenza di un lag con FPS uniformemente basso, il rallentamento ha una variabilità pronunciata.
Quando un’app rallenta, l’utente non comprende la logica dei rallentamenti: lo schermo può scorrere fluidamente e poi fermarsi improvvisamente per un secondo. Questo causa frustrazione e riduce la fiducia nell’app. Secondo Google, il 53% degli utenti abbandona un sito o un’app se il caricamento richiede più di 3 secondi.
La natura intermittente del rallentamento indica che il problema è causato da fattori guidati da eventi, non da sovraccarico costante. Esaminiamo gli scenari tipici.
Su Android, nell’ambiente ART, la garbage collection ferma tutti i thread dell’app. Se il codice crea molti oggetti temporanei — ad esempio, creando una nuova String tramite concatenazione a ogni chiamata onBindViewHolder — il GC viene eseguito più frequentemente. Una pausa può durare 5–50 ms a seconda della dimensione dell’heap e della generazione degli oggetti. L’utente percepisce questo come un improvviso “pensare”.
Room su Android e Core Data su iOS supportano query asincrone, ma gli sviluppatori spesso chiamano getValue() o eseguono query tramite runBlocking per semplicità. Una SELECT pesante con join su una tabella di 10.000 righe può richiedere 200–500 ms, bloccando completamente l’UI durante quel periodo.
Caricare un’immagine della fotocamera (12 MP, 4000x3000 px) senza ridimensionamento richiede fino a 200 ms per la decodifica in Bitmap. Se le immagini vengono caricate in modo asincrono ma senza un pool di thread limitato, eseguire 5–6 decodifiche contemporaneamente può sovraccaricare la CPU, causando rallentamenti migratori.
Diagnosticare i rallentamenti intermittenti è più difficile che diagnosticare i lag costanti perché il problema potrebbe non riprodursi a ogni esecuzione. È necessaria la raccolta di statistiche per un lungo periodo.
Android Studio Memory Profiler mostra non solo l’utilizzo della memoria ma anche gli eventi GC: frequenza, tipo (Concurrent, Full), durata. Se il GC si verifica più di una volta ogni 5 secondi in stato inattivo — è un segno di allocazione eccessiva. Eseguire un heap dump al momento del rallentamento rivela quali oggetti stanno occupando la memoria.
Su iOS, usa il modello Allocations in Instruments per tracciare la creazione e il rilascio degli oggetti. Attiva Generations — permettono di fare snapshot dell’heap tra le azioni e vedere quali oggetti rimangono in memoria. Gli oggetti persistenti che non vengono rilasciati sono una fonte di accumulo di memoria e successive pause.
JankStats è una libreria Android che raccoglie metriche dei frame saltati in tempo reale. Associa ogni jank allo scenario corrente (ad esempio, “scorrimento elenco”, “apertura schermata”), permettendo di capire quale azione specifica innesca il rallentamento.
Esempio di integrazione di JankStats per tracciare i blocchi su 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")
}
}
}
}
Eliminare i rallentamenti richiede un lavoro mirato su ogni causa. Non esiste una soluzione universale — è necessaria l’analisi di profili di prestazioni specifici.
Se un elenco contiene 1000+ elementi e tutti vengono caricati in una volta — è un rallentamento garantito. Paging 3 su Android e NSFetchedResultsController su iOS caricano i dati in porzioni man mano che l’utente scorre. L’utente vede solo i primi 10–20 elementi; il resto viene caricato in background.
Room permette di profilare le query tramite lo Strumento di Ispezione in Android Studio: tempo di esecuzione, numero di righe restituite e piano di query sono visibili. L’aggiunta di indici sulle colonne WHERE e ORDER BY può ridurre il tempo di query da 300 ms a 5 ms. Su iOS, un controllo simile viene eseguito da Core Data Profiler in Instruments.
Sincronizzazioni in background, download di file, elaborazione dati — tutto questo dovrebbe essere eseguito tramite WorkManager (Android) o Background Tasks (iOS). Se la sincronizzazione viene eseguita nel thread UI, l’app rallenterà durante l’esecuzione. WorkManager garantisce l’esecuzione su un thread in background con consapevolezza dello stato della batteria e della rete.
Esempio di sincronizzazione in background tramite WorkManager su 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()
}
}
}
I rallentamenti possono essere prevenuti in fase di codifica seguendo i principi di una gestione efficiente della memoria e dei thread.
Baseline Profiles sono un elenco di classi e metodi che Android compila in anticipo (AOT) anziché JIT. Senza un profilo, ogni nuova schermata viene compilata alla prima apertura, causando un ritardo di 100–500 ms. Prepara un Baseline Profile per le schermate chiave e attiva la generazione in Gradle tramite il plugin baseline-profile-gradle-plugin.
Un percorso caldo è il codice eseguito su ogni frame: onBindViewHolder, draw, layoutSubviews. Evita di creare oggetti in questi metodi: usa pool di oggetti, StringBuilder invece della concatenazione, memorizza nella cache stringhe formattate e formattatori. Ogni allocazione extra avvicina il prossimo GC.
Aggiungi Macrobenchmark alla tua pipeline CI con uno scenario di scorrimento elenco e apertura schermata. Imposta una soglia: il 99° percentile del tempo di frame non deve superare 16 ms. Se la soglia viene superata — la build viene rifiutata fino all’ottimizzazione.
Domande frequenti
Un lag è un ritardo costante (ad esempio, 200 ms a ogni tocco). Rallentamento è intermittente: l’app funziona normalmente, poi rallenta improvvisamente per 1–3 secondi, poi torna normale. La causa sono fattori guidati da eventi come pause GC o query sincrone al database.
Usa Memory Profiler in Android Studio: la scheda Memory mostra gli eventi GC con durata. Per il monitoraggio in produzione, integra Firebase Performance Monitoring con trace personalizzati. Su iOS, attiva Malloc Debug e segna le generazioni di allocazione in Instruments.
Indirettamente — sì. Se la risposta del server è in ritardo e l’UI la attende in modo sincrono, l’app si blocca. Se la richiesta è asincrona ma l’elaborazione della risposta viene eseguita nel thread UI — anche questo causerà rallentamenti. La soluzione è l’elaborazione asincrona con coroutine e indicatori di progresso.
Se usato in modo errato, KMP può generare oggetti wrapper eccessivi per l’interoperabilità. Su iOS, questo aumenta la frequenza di allocazione e, di conseguenza, le pause ARC. Usa @ObjCName, ottimizza expect/actual ed evita chiamate frequenti al codice condiviso dai percorsi caldi dell’UI.
Aumentare l’heap tramite android:largeHeap=”true” ritarda il GC ma non elimina la causa delle allocazioni. Quando il GC viene infine eseguito, la pausa sarà più lunga perché più oggetti devono essere attraversati. La soluzione è ridurre il numero di allocazioni, non espandere l’heap.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche