À la traîne est la description par l’utilisateur d’une situation où une application mobile fonctionne lentement et de manière irrégulière : tantôt elle répond normalement, tantôt elle se fige soudainement pendant plusieurs secondes. Dans un contexte technique, « à la traîne » signifie une combinaison de lags et de micro-freezes causée par des pauses GC fréquentes, le blocage du thread principal par des opérations synchrones et des structures de données sous-optimales. Selon le Android Performance Benchmarking Guide, réduire le temps de réponse de 300 ms à 100 ms augmente la rétention des utilisateurs de 25 %. Diagnostiquer les ralentissements nécessite une combinaison de profilage CPU et Mémoire avec une analyse de la fréquence du ramasse-miettes.
Points clés
À la traîne est un terme informel que les utilisateurs emploient pour décrire des performances d’application subjectivement lentes. Contrairement à un lag, qui se manifeste comme un retard constant, être à la traîne consiste en des freeze irréguliers : l’application peut fonctionner parfaitement pendant plusieurs secondes, puis « réfléchir » pendant 1–3 secondes.
Du point de vue du profilage, être à la traîne se manifeste comme une série d’images sautées (jank) avec des pics de retard supérieurs à 100 ms. Sur un graphique FPS, cela ressemble à des chutes brutales : 60 → 20 → 55 → 10 images par seconde. Contrairement à un lag avec un FPS uniformément bas, être à la traîne a une variabilité prononcée.
Quand une application rame, l’utilisateur ne comprend pas la logique des ralentissements : l’écran peut défiler en douceur puis s’arrêter soudainement pendant une seconde. Cela provoque de la frustration et réduit la confiance dans l’application. Selon Google, 53 % des utilisateurs quittent un site ou une application si le chargement prend plus de 3 secondes.
La nature intermittente des ralentissements indique que le problème est causé par des facteurs pilotés par les événements, et non par une surcharge constante. Examinons les scénarios typiques.
Sous Android, dans l’environnement ART, le ramasse-miettes arrête tous les threads de l’application. Si le code crée beaucoup d’objets temporaires — par exemple, en créant une nouvelle String par concaténation à chaque appel onBindViewHolder — le GC s’exécute plus fréquemment. Une pause peut durer 5–50 ms selon la taille du heap et la génération d’objets. L’utilisateur perçoit cela comme une « réflexion » soudaine.
Room sur Android et Core Data sur iOS prennent en charge les requêtes asynchrones, mais les développeurs appellent souvent getValue() ou exécutent des requêtes via runBlocking par simplicité. Un SELECT lourd avec des joins sur une table de 10 000 lignes peut prendre 200–500 ms, bloquant complètement l’UI pendant ce temps.
Charger une image d’appareil photo (12 Mpx, 4000×3000 px) sans mise à l’échelle prend jusqu’à 200 ms pour le décodage en Bitmap. Si les images sont chargées de manière asynchrone mais sans pool de threads limité, lancer 5–6 décodages simultanés peut surcharger le CPU, provoquant des ralentissements migrateurs.
Diagnostiquer les ralentissements intermittents est plus difficile que de diagnostiquer les lags constants, car le problème peut ne pas se reproduire à chaque exécution. Une collecte de statistiques sur une longue période est nécessaire.
Android Studio Memory Profiler montre non seulement l’utilisation de la mémoire, mais aussi les événements GC : fréquence, type (Concurrent, Full), durée. Si le GC se produit plus d’une fois toutes les 5 secondes en état inactif — c’est un signe d’allocation excessive. Prendre un heap dump au moment du ralentissement permet de voir quels objets occupent la mémoire.
Sur iOS, utilisez le modèle Allocations dans Instruments pour suivre la création et la libération d’objets. Activez Generations — elles permettent de prendre des instantanés du heap entre les actions et de voir quels objets restent en mémoire. Les objets persistants qui ne sont pas libérés sont une source d’accumulation de mémoire et de pauses ultérieures.
JankStats est une bibliothèque Android qui collecte des métriques d’images sautées en temps réel. Elle associe chaque jank au scénario en cours (par exemple, « défilement de liste », « ouverture d’écran »), ce qui permet de comprendre quelle action spécifique déclenche les ralentissements.
Exemple d’intégration de JankStats pour le suivi des freeze sur 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")
}
}
}
}
Éliminer les ralentissements nécessite un travail ciblé sur chaque cause. Il n’existe pas de solution universelle — une analyse des profils de performance spécifiques est nécessaire.
Si une liste contient 1000+ éléments et que tous sont chargés en une fois — c’est un ralentissement garanti. Paging 3 sur Android et NSFetchedResultsController sur iOS chargent les données par portions au fur et à mesure que l’utilisateur fait défiler. L’utilisateur ne voit que les 10–20 premiers éléments ; le reste est chargé en arrière-plan.
Room permet de profiler les requêtes via l’outil d’inspection dans Android Studio : le temps d’exécution, le nombre de lignes renvoyées et le plan de requête sont visibles. L’ajout d’index sur les colonnes WHERE et ORDER BY peut réduire le temps de requête de 300 ms à 5 ms. Sur iOS, une vérification similaire est effectuée par Core Data Profiler dans Instruments.
Les synchronisations en arrière-plan, les téléchargements de fichiers, le traitement de données — tout cela doit être exécuté via WorkManager (Android) ou Background Tasks (iOS). Si la synchronisation s’exécute sur le thread UI, l’application rame pendant l’exécution. WorkManager garantit l’exécution sur un thread d’arrière-plan en tenant compte de l’état de la batterie et du réseau.
Exemple de synchronisation en arrière-plan via WorkManager sur 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()
}
}
}
On peut prévenir les ralentissements dès la phase de codage en suivant les principes d’une gestion efficace de la mémoire et des threads.
Baseline Profiles sont une liste de classes et de méthodes qu’Android compile à l’avance (AOT) plutôt qu’en JIT. Sans profil, chaque nouvel écran est compilé lors de sa première ouverture, provoquant un retard de 100–500 ms. Préparez un Baseline Profile pour les écrans clés et activez la génération dans Gradle via le plugin baseline-profile-gradle-plugin.
Un chemin chaud est un code exécuté à chaque image : onBindViewHolder, draw, layoutSubviews. Évitez de créer des objets dans ces méthodes : utilisez des pools d’objets, StringBuilder au lieu de concaténation, mettez en cache les chaînes formatées et les formateurs. Chaque allocation supplémentaire rapproche le prochain GC.
Ajoutez Macrobenchmark à votre pipeline CI avec un scénario de défilement de liste et d’ouverture d’écran. Définissez un seuil : le 99e centile du temps d’image ne doit pas dépasser 16 ms. Si le seuil est dépassé — la build est rejetée jusqu’à l’optimisation.
Questions fréquentes
Un lag est un retard constant (par exemple, 200 ms à chaque tape). Être à la traîne est intermittent : l’application fonctionne normalement, puis ralentit soudainement pendant 1–3 secondes, puis revient à la normale. La cause est des facteurs pilotés par les événements comme les pauses GC ou les requêtes synchrones à la base de données.
Utilisez Memory Profiler dans Android Studio : l’onglet Memory montre les événements GC avec la durée. Pour la surveillance en production, intégrez Firebase Performance Monitoring avec des traces personnalisées. Sur iOS, activez Malloc Debug et marquez les générations d’allocations dans Instruments.
Indirectement — oui. Si la réponse du serveur est retardée et que l’UI l’attend de manière synchrone, l’application se fige. Si la requête est asynchrone mais que le traitement de la réponse est effectué dans le thread UI — cela provoquera également des ralentissements. La solution est un traitement asynchrone avec des coroutines et des indicateurs de progression.
Lorsqu’il est utilisé incorrectement, KMP peut générer des objets wrapper excessifs pour l’interopérabilité. Sur iOS, cela augmente la fréquence des allocations et, par conséquent, les pauses ARC. Utilisez @ObjCName, optimisez expect/actual et évitez les appels fréquents au code partagé depuis les chemins chauds de l’UI.
Augmenter le heap via android:largeHeap=”true” retarde le GC mais n’élimine pas la cause des allocations. Lorsque le GC s’exécute finalement, la pause sera plus longue car davantage d’objets doivent être parcourus. La solution est de réduire le nombre d’allocations, pas d’étendre le heap.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi