MFormations
Modern Mobile Engineering

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

  1. Les fondamentaux du rendu
  2. Mémoire
  3. Démarrage à froid
  4. Profiling
  5. Performances réseau
  6. Optimisation des images
  7. Lazy loading et listes
  8. Performances de la base de données
  9. 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

  1. Input : touch → événement.
  2. Layout : calcul des positions (Measure/Layout).
  3. Draw : génération des instructions de rendu.
  4. Raster : rendu GPU.
  5. 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.

NiveauCouleurAction
BlancOK
VertÀ surveiller
RoseOptimiser
4×+RougeInterdit

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

  • setState reconstruit le widget → limiter le périmètre (const constructors, RepaintBoundary).
  • const est GRATUIT à la compilation → l'utiliser partout où possible.
  • ListView.builder construit à 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 :

  1. Contextes : une Activity référencée par un singleton.
  2. Listeners non déréférencés.
  3. Coroutines/async non annulées.
  4. Images : décodées en pleine taille, jamais recyclées.
  5. 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=true est 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/cacheHeight pour décoder à la bonne taille.

2.5 Outils de détection

PlateformeOutilCe qu'il montre
AndroidProfiler → MemoryHeap, objets, allocation tracking
AndroidLeakCanaryDétection automatique des fuites
iOSInstruments → LeaksCycles de référence
iOSXcode Memory DebuggerGraphe de rétention
FlutterDevTools → MemoryHeap, leak tracking
RNReact DevTools / hermesProfiler 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 :

  1. Ouvrir LeakCanary/Instruments.
  2. Identifier l'objet leaked.
  3. Remonter les références jusqu'à la racine (Application/global).
  4. 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

  1. Init synchrones dans Application.onCreate (SDK, DB, DI lourde).
  2. Layouts complexes à l'écran d'accueil.
  3. Décodage d'images en grand au premier écran.
  4. 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 windowBackground devient 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 AppIcon et 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

VoletsUsage
CPUCoroutines, méthodes lentes (System Trace)
MemoryHeap, allocations, fuites
NetworkRequêtes, tailles, latence
EnergyConsommation
GPU / FrameJank, frame times

Workflow :

  1. Reproduire le scénario.
  2. Lancer le recording (CPU/System Trace).
  3. Identifier le hotspot.
  4. Corriger.
  5. Re-mesurer (avant/après).

4.2 Xcode Instruments

InstrumentUsage
Time ProfilerMéthodes lentes CPU
AllocationsMémoire, fuites
LeaksCycles de référence
Core AnimationFPS, rendu
NetworkRequêtes (avec condition)
App LaunchDémarrage

4.3 Flutter DevTools

VoletUsage
PerformanceFrame build/raster times, jank
MemoryHeap + leaks
NetworkRequêtes HTTP
TimelineEvent 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)

  • Hermes avec --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

LevierEffet
Compresser (gzip)-80 % de payload
Cache HTTP (ETag)304 → zéro transfert
HTTP/2 / HTTP/3Multiplexing, 0-RTT
PaginationCharge utile réduite
CDNLatence réduite
BatchingMoins de round-trips
Keep-alive / poolingRé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

  1. Redimensionner côté serveur/CDN (jamais télécharger 4000px pour un avatar 96px).
  2. Formats modernes : WebP/AVIF (~50 % plus léger que JPEG/PNG).
  3. Downsampling au décodage (BitmapFactory inSampleSize, UIImage downsampling, cacheWidth Flutter).
  4. Cacher : mémoire + disque via la lib d'images (chapitre 12).
  5. Progressive/placeholder : afficher un blur small → image réelle.
  6. 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

PlateformeComposant
Android XMLRecyclerView (ViewHolder + DiffUtil)
Android ComposeLazyColumn / LazyRow
iOS UIKitUICollectionView / UITableView (cell reuse)
iOS SwiftUIList / LazyVStack / LazyHStack
FlutterListView.builder / GridView.builder
React NativeFlatList / 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.memo sur la ligne.
  • Flutter : const constructors et RepaintBoundary.

8. Performances de la base de données

8.1 Les règles SQL

  1. Indexer les colonnes de WHERE, JOIN, ORDER BY.
  2. Éviter SELECT * quand on peut cibler.
  3. Bulk inserts en transaction unique (pas 10 000 inserts isolés).
  4. Éviter les requêtes sur le main thread (toujours suspend/background).
  5. 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 / NSBatchDeleteRequest pour les updates massifs (évite de charger les objets en mémoire).
  • fetchBatchSize pour les gros fetch.
  • Pré-construire les requêtes (fetchRequest réutilisables).

8.5 Watchers et requêtes en arrière-plan

  • Android : les requêtes DB dans Dispatchers.IO ou 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étriqueOutil
Cold start p95Firebase Performance, Xcode Metrics
ANR (Android)Firebase Crashlytics / Play Console
App launch (iOS)Xcode Organizer / MetricKit
Jank / dropped framesProfilers, Firebase
OOMCrashlytics (out of memory)
Taille APK/IPABuild reports

9.3 Les pièges courants

  • Optimiser sans mesurer.
  • largeHeap=true comme 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 :

  1. 16.6 ms par frame — tout ce qui dépasse crée du jank.
  2. Mesurer avant d'optimiser ; toujours des avant/après.
  3. Les fuites mémoire se traquent avec un graphe de rétention.
  4. Images = downsampling + formats modernes + cache.
  5. Listes = lazy + clés stables.
  6. 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).