Chapitre 14
14 - Performance Mobile
14 - Performance Mobile
14 - Performance Mobile : Cours complet
Niveau : Intermédiaire → Avancé Durée estimée : 6 h Objectif : faire tourner une app mobile fluide, économe en mémoire et à démarrage rapide, et savoir la profiler.
Table des matières
- Les fondamentaux du rendu
- Mémoire
- Démarrage à froid
- Profiling
- Performances réseau
- Optimisation des images
- Lazy loading et listes
- Performances de la base de données
- Best practices
1. Les fondamentaux du rendu
1.1 Le budget de frame
Un écran à 60 Hz affiche une image toutes les 16.6 ms. À 120 Hz (ProMotion), c'est 8.3 ms. Si le pipeline de rendu dépasse ce budget, on droppe des frames → jank (saccades).
Diagramme en cours de génération...
1.2 Le pipeline
- Input : touch → événement.
- Layout : calcul des positions (Measure/Layout).
- Draw : génération des instructions de rendu.
- Raster : rendu GPU.
- Composition : assemblage des couches.
Si le step 3 (ou n'importe quel) dépasse le budget → frame dropped.
1.3 Jank et dropped frames
Mesuré par :
- Choreographer / FrameMetrics (Android)
- CADisplayLink (iOS)
- Flutter DevTools (Frame times)
Diagramme en cours de génération...
1.4 Overdraw
L'overdraw = peindre des pixels plusieurs fois. Android la mesure dans Developer Options → Show GPU Overdraw.
| Niveau | Couleur | Action |
|---|---|---|
| 1× | Blanc | OK |
| 2× | Vert | À surveiller |
| 3× | Rose | Optimiser |
| 4×+ | Rouge | Interdit |
Causes : backgrounds superposés, windowBackground + fond de layout + fond de composant, ombres inutiles.
1.5 Les outils visuels Android
# Vérifier le jank sur device
adb shell dumpsys gfxinfo <package> framestats
# (Compose) Frame timings
adb shell dumpsys gfxinfo <package> reset
Android Studio → Layout Inspector et Profiler (GPU).
1.6 iOS — Instruments Core Animation
Instrument Core Animation montre :
- FPS (frames par seconde)
- Core Animation FPS : doit rester ≥ 55-60
- CA transactions : temps de layout/draw
1.7 Flutter — le coût de la reconstruction
setStatereconstruit le widget → limiter le périmètre (constconstructors,RepaintBoundary).constest GRATUIT à la compilation → l'utiliser partout où possible.ListView.builderconstruit à la demande (pavé 8 ci-dessous).
2. Mémoire
2.1 Les fuites mémoire
Une fuite = un objet gardé vivant par une référence alors qu'il ne sert plus. Conséquences : pression mémoire, GC fréquent, OOM (OutOfMemoryError), app tuée.
Causes classiques :
- Contextes : une Activity référencée par un singleton.
- Listeners non déréférencés.
- Coroutines/async non annulées.
- Images : décodées en pleine taille, jamais recyclées.
- Objets lourds gardés en statique.
2.2 Le cas classique Android — Activity dans un singleton
object Cache {
var activity: Activity? = null // ⛔ fuite : l'Activity ne sera jamais GC
}
Correctif : jamais de référence à une Activity dans un objet global. Utiliser ApplicationContext ou un WeakReference conscient.
2.3 Les fuites iOS — closures et cycles
Swift : un [weak self] oublié crée un cycle de référence :
final class HomeViewModel: ObservableObject {
var callback: (() -> Void)?
func setup() {
callback = {
self.state = .loaded // ⛔ cycle : viewModel -> closure -> self
}
}
}
func setup() {
callback = { [weak self] in
self?.state = .loaded // ✅ pas de cycle
}
}
[weak self] ou [unowned self] selon le cycle de vie.
2.4 OOM et gestion des images
- Android :
largeHeap=trueest un mauvais correctif (retarde le problème, OOM ailleurs). Mieux vaut réduire la consommation (downsampling des images). - iOS : les images décodées en mémoire =
width × height × 4 bytes. Une image 4000×3000 = 48 Mo. Ne jamais décoder en pleine taille pour une vignette. - Flutter :
cacheWidth/cacheHeightpour décoder à la bonne taille.
2.5 Outils de détection
| Plateforme | Outil | Ce qu'il montre |
|---|---|---|
| Android | Profiler → Memory | Heap, objets, allocation tracking |
| Android | LeakCanary | Détection automatique des fuites |
| iOS | Instruments → Leaks | Cycles de référence |
| iOS | Xcode Memory Debugger | Graphe de rétention |
| Flutter | DevTools → Memory | Heap, leak tracking |
| RN | React DevTools / hermes | Profiler JS |
2.6 Le graphe de rétention
Un leak = un chemin de racine → objet qui ne devrait pas exister. L'analyse consiste à remonter ce chemin :
- Ouvrir LeakCanary/Instruments.
- Identifier l'objet leaked.
- Remonter les références jusqu'à la racine (Application/global).
- Casser la référence fautive (weak, removeListener, annuler la coroutine).
3. Démarrage à froid
3.1 Le cycle de démarrage
Diagramme en cours de génération...
Métriques :
- Cold start : process froid.
- Warm start : process en vie, Activity recréée.
- Hot start : app au premier plan (Android).
- TTI (Time To Interactive) : premier contenu utile + interactions réactives.
3.2 Les 4 causes de démarrage lent
- Init synchrones dans
Application.onCreate(SDK, DB, DI lourde). - Layouts complexes à l'écran d'accueil.
- Décodage d'images en grand au premier écran.
- Réseau bloquant (fetch avant affichage).
3.3 Optimisations
Android — Application :
- Déléguer les init lourdes (WorkManager, lazy) →
initializeOnStartup=false. - Hilt/Dagger : modules lazy.
Android — Theme/Splash (Android 12+ SplashScreen API) :
- Le
windowBackgrounddevient le splash → le garder léger.
<style name="Theme.TaskFlow" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/bg</item>
<item name="postSplashScreenTheme">@style/Theme.TaskFlow.Main</item>
</style>
iOS — SwiftUI/UIKit :
- Pas de travail lourd dans
App.init/AppDelegate.applicationDidFinishLaunching. - Déléguer aux tasks d'arrière-plan.
- Assets
AppIconet splash (LaunchScreen) en static (pas de réseau).
Flutter :
- Lancer les fetch après le premier frame (
WidgetsBinding.instance.addPostFrameCallback). - Utiliser des placeholders et des futures.
3.4 Mesurer
# Android
adb shell am start -W -n package/.MainActivity
# -> TotalTime, WaitTime
# iOS (Xcode Instruments → App Launch template)
4. Profiling
4.1 Android Studio Profiler
| Volets | Usage |
|---|---|
| CPU | Coroutines, méthodes lentes (System Trace) |
| Memory | Heap, allocations, fuites |
| Network | Requêtes, tailles, latence |
| Energy | Consommation |
| GPU / Frame | Jank, frame times |
Workflow :
- Reproduire le scénario.
- Lancer le recording (CPU/System Trace).
- Identifier le hotspot.
- Corriger.
- Re-mesurer (avant/après).
4.2 Xcode Instruments
| Instrument | Usage |
|---|---|
| Time Profiler | Méthodes lentes CPU |
| Allocations | Mémoire, fuites |
| Leaks | Cycles de référence |
| Core Animation | FPS, rendu |
| Network | Requêtes (avec condition) |
| App Launch | Démarrage |
4.3 Flutter DevTools
| Volet | Usage |
|---|---|
| Performance | Frame build/raster times, jank |
| Memory | Heap + leaks |
| Network | Requêtes HTTP |
| Timeline | Event de cycle de vie |
La barre verte/rouge de DevTools montre si le budget frame est respecté (vertical) par frame.
4.4 Profiling React Native (Hermes)
Hermesavec--profile(sampling CPU).- React DevTools profiler (composants re-rendus).
- Perf monitor de DevTools (FPS).
4.5 La règle d'or du profiling
Mesurer d'abord, optimiser ensuite. 90 % des optimisations intuitives sont du bruit. Le profiler dit la vérité.
Toujours enregistrer un avant/après avec la même scénario.
5. Performances réseau
5.1 Les leviers
| Levier | Effet |
|---|---|
| Compresser (gzip) | -80 % de payload |
| Cache HTTP (ETag) | 304 → zéro transfert |
| HTTP/2 / HTTP/3 | Multiplexing, 0-RTT |
| Pagination | Charge utile réduite |
| CDN | Latence réduite |
| Batching | Moins de round-trips |
| Keep-alive / pooling | Réutiliser les connexions |
5.2 Android — cache OkHttp
val client = OkHttpClient.Builder()
.cache(Cache(context.cacheDir, 50L * 1024 * 1024))
.build()
5.3 iOS — URLSession caching
let config = URLSessionConfiguration.default
config.requestCachePolicy = .useProtocolCachePolicy
config.urlCache = URLCache(memoryCapacity: 20_000_000,
diskCapacity: 100_000_000)
5.4 Batching et déduplication
- Grouper les petites requêtes (sync en une fois).
- Dédupliquer les requêtes identiques en vol (même URL en cours de chargement → partager le même Future/Promise).
5.5 WebSocket vs polling
Pour du temps réel : WebSocket (chapitre 12) évite le polling coûteux en batterie et en réseau.
6. Optimisation des images
6.1 Le pipeline optimal
Diagramme en cours de génération...
6.2 Les règles
- Redimensionner côté serveur/CDN (jamais télécharger 4000px pour un avatar 96px).
- Formats modernes : WebP/AVIF (~50 % plus léger que JPEG/PNG).
- Downsampling au décodage (BitmapFactory inSampleSize, UIImage downsampling, cacheWidth Flutter).
- Cacher : mémoire + disque via la lib d'images (chapitre 12).
- Progressive/placeholder : afficher un blur small → image réelle.
- Purge du cache disque selon une taille max.
6.3 Android — BitmapFactory downsampling
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(path, bounds)
val sample = maxOf(1, bounds.outWidth / targetWidth)
val opts = BitmapFactory.Options().apply { inSampleSize = sample }
val bitmap = BitmapFactory.decodeFile(path, opts)
6.4 iOS — CGImageSource downsampling
let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
let thumb = CGImageSourceCreateThumbnailAtIndex(
source, 0,
[kCGImageSourceThumbnailMaxPixelSize: 400] as CFDictionary
)
6.5 Lazy loading des images (voir section 7)
7. Lazy loading et listes
7.1 Le principe
Ne construire que les éléments visibles (+ une marge de préfetch). Une liste de 100 000 éléments ne doit jamais construire 100 000 vues.
7.2 Les outils
| Plateforme | Composant |
|---|---|
| Android XML | RecyclerView (ViewHolder + DiffUtil) |
| Android Compose | LazyColumn / LazyRow |
| iOS UIKit | UICollectionView / UITableView (cell reuse) |
| iOS SwiftUI | List / LazyVStack / LazyHStack |
| Flutter | ListView.builder / GridView.builder |
| React Native | FlatList / SectionList (VirtualizedList) |
7.3 Compose — LazyColumn
val tickets by viewModel.state.collectAsStateWithLifecycle()
LazyColumn {
items(tickets, key = { it.id }) { ticket ->
TicketRow(ticket)
}
}
Le key (stable) évite les reconstructions inutiles lors des re-ordres.
7.4 Flutter — ListView.builder
ListView.builder(
itemCount: tickets.length,
itemBuilder: (context, index) => TicketTile(tickets[index]),
)
7.5 React Native — FlatList
<FlatList
data={tickets}
keyExtractor={(t) => t.id}
renderItem={({ item }) => <TicketRow ticket={item} />}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={21}
/>
7.6 DiffUtil et les clés stables
Le recyclage n'évite le re-rendu que si les clés sont stables et que les éléments sont mémoïsés :
- Android Compose :
key = { it.id }. - RN :
keyExtractor+React.memosur la ligne. - Flutter :
constconstructors etRepaintBoundary.
8. Performances de la base de données
8.1 Les règles SQL
- Indexer les colonnes de
WHERE,JOIN,ORDER BY. - Éviter
SELECT *quand on peut cibler. - Bulk inserts en transaction unique (pas 10 000 inserts isolés).
- Éviter les requêtes sur le main thread (toujours suspend/background).
- Attention au N+1 : une requête par ligne → utiliser JOIN/relations.
8.2 Room — bulk insert transaction
@Dao
interface TicketDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(tickets: List<TicketEntity>)
}
// Room exécute l'insertion en une seule transaction SQLite
// (comparer avec un insert dans une boucle : 10× plus lent)
8.3 Room — requêtes coûteuses
-- N+1 anti-pattern
SELECT * FROM tickets; -- 1 requête
-- puis pour CHAQUE ticket :
SELECT * FROM projects WHERE id=?; -- N requêtes !!
-- Solution : jointure
SELECT t.*, p.name FROM tickets t
JOIN projects p ON p.id = t.projectId;
8.4 Core Data / SQLite iOS
NSBatchUpdateRequest/NSBatchDeleteRequestpour les updates massifs (évite de charger les objets en mémoire).fetchBatchSizepour les gros fetch.- Pré-construire les requêtes (
fetchRequestréutilisables).
8.5 Watchers et requêtes en arrière-plan
- Android : les requêtes DB dans
Dispatchers.IOou via le DAO suspend (Room le gère). - iOS : fetch dans un background context, merge dans viewContext.
9. Best practices
9.1 La checklist de performance
Rendu
- Frame budget respecté (aucun jank sur les parcours clés)
- Pas d'overdraw rouge/rose
- Listes lazy (pas de build de tout)
- Animations légères (transforms > layout)
Mémoire
- Pas de fuite détectée (LeakCanary / Instruments)
- Images downsamplées
- Coroutines/async annulées dans les cycles de vie
Démarrage
- Init déléguées (lazy)
- Splash léger
- TTI mesuré et enregistré
Réseau
- Compression, cache HTTP, pagination
- Batching + déduplication
Base de données
- Index présents
- Bulk inserts
- Aucune requête sur le main thread
9.2 Les métriques à surveiller (observabilité)
| Métrique | Outil |
|---|---|
| Cold start p95 | Firebase Performance, Xcode Metrics |
| ANR (Android) | Firebase Crashlytics / Play Console |
| App launch (iOS) | Xcode Organizer / MetricKit |
| Jank / dropped frames | Profilers, Firebase |
| OOM | Crashlytics (out of memory) |
| Taille APK/IPA | Build reports |
9.3 Les pièges courants
- Optimiser sans mesurer.
largeHeap=truecomme solution (⚠).getExternalStorage/ IO synchrone sur main thread.- Construire des listes entières.
- Décompresser des images pleine taille.
10. Synthèse
Diagramme en cours de génération...
Points à retenir :
- 16.6 ms par frame — tout ce qui dépasse crée du jank.
- Mesurer avant d'optimiser ; toujours des avant/après.
- Les fuites mémoire se traquent avec un graphe de rétention.
- Images = downsampling + formats modernes + cache.
- Listes = lazy + clés stables.
- Le démarrage à froid se joue dans les init et le premier écran.
Aller plus loin
- Chapitre 15 (Sécurité) : l'optimisation ne doit pas casser la sécurité.
- Chapitre 16 (DevOps) : observabilité des métriques de perf en production.
- Chapitre 18 (Projet-Fil-Rouge) : perf de TaskFlow (listes, images, démarrage).