flutter-startup-network-blocking

Par divinevideo · divine-mobile

Corrigez le démarrage lent d'une application Flutter causé par des opérations réseau bloquantes lors de l'initialisation. À utiliser quand : (1) l'application met plusieurs secondes à afficher la première frame, (2) les logs de démarrage montrent des opérations réseau séquentielles (connexions WebSocket, appels API), (3) des services s'initialisent au démarrage alors qu'ils ne sont nécessaires qu'après l'authentification de l'utilisateur. Couvre : la conversion d'opérations réseau séquentielles en parallèle avec `Future.wait()`, le report de l'initialisation des services jusqu'au moment où ils sont réellement nécessaires (lazy init), et l'identification des opérations bloquantes dans les chaînes d'initialisation Riverpod/provider.

npx skills add https://github.com/divinevideo/divine-mobile --skill flutter-startup-network-blocking

Flutter Blocage Réseau au Démarrage

Problème

Le démarrage de l'application Flutter est lent car les opérations réseau (connexions WebSocket, appels API, initialisation de services) s'exécutent séquentiellement pendant la phase d'initialisation, bloquant le rendu de la première frame. Avec plusieurs connexions relay/serveur, le pire cas de temps de démarrage devient O(n × timeout) au lieu de O(max timeout).

Contexte / Conditions Déclencheurs

  • L'app prend 3+ secondes pour afficher la première frame au lancement
  • Les logs de démarrage montrent des "connecting to relay X... connecting to relay Y..." séquentiels
  • Les services nécessitant une authentification s'initialisent même pour les utilisateurs non authentifiés
  • _initializeCoreServices() ou similaire contient plusieurs opérations réseau en attente
  • Les providers Riverpod initialisent avidement les services dépendants du réseau dans leur méthode build()

Solution

1. Identifier les Opérations Bloquantes

Cherchez les awaits séquentiels dans le code de démarrage :

// MAUVAIS : Séquentiel - chaque connexion bloque la suivante
for (final url in relays) {
  await connectToRelay(url);  // Bloque le démarrage !
}

2. Convertir Séquentiel en Parallèle

Utilisez Future.wait() pour exécuter toutes les connexions simultanément :

// BON : Parallèle - toutes les connexions s'exécutent à la fois
final results = await Future.wait(
  relays.map((url) async {
    final success = await connectToRelay(url);
    return MapEntry(url, success);
  }),
);

3. Reporter l'Initialisation des Services Non-Critiques

Déplacez les services dépendants du réseau hors du chemin critique de démarrage :

Avant (bloque le démarrage) :

Future<void> _initializeCoreServices(ProviderContainer container) async {
  await container.read(authServiceProvider).initialize();
  await container.read(nostrServiceProvider).initialize();  // BLOQUE pour les connexions relay !
  await container.read(otherServiceProvider).initialize();
}

Après (initialisation lazy) :

Future<void> _initializeCoreServices(ProviderContainer container) async {
  // NOTE : NostrService s'initialise paresseusement quand l'utilisateur s'authentifie
  await container.read(authServiceProvider).initialize();
  await container.read(seenVideosServiceProvider).initialize();
  // NostrService NON initialisé ici - s'exécute quand l'état d'auth change
}

4. Utiliser les Dépendances Provider pour l'Init Lazy

Laissez Riverpod gérer l'initialisation paresseuse via les dépendances provider :

@riverpod
NostrClient nostrClient(NostrClientRef ref) {
  final authService = ref.watch(authServiceProvider);

  // Crée le client uniquement quand l'état d'auth est prêt
  if (!authService.isAuthenticated) {
    return NostrClient.disconnected();
  }

  // Initialise paresseusement quand vraiment nécessaire
  final client = NostrClient(relays: authService.userRelays);
  Future.microtask(() => client.initialize());
  return client;
}

Vérification

  1. Vérifiez les logs de démarrage pour "First frame rendered in Xms" - doit être < 2000ms
  2. Vérifiez que les opérations réseau se produisent APRÈS le timestamp de première frame dans les logs
  3. Pour les utilisateurs non authentifiés, les connexions relay ne doivent PAS apparaître dans les logs de démarrage

Exemple

Amélioration de démarrage réalisée :

  • Avant : Première frame à 3500ms+ (attente de 5 relays × ~700ms chacun)
  • Après : Première frame à 1426ms (les connexions parallèles se font post-frame)

Motif de log montrant que la correction fonctionne :

[18:15:17.538] First frame rendered in 1426ms
[18:15:17.556] Creating NostrClient...  // APRÈS la première frame !

Notes

  • Ce motif s'applique à toute initialisation asynchrone, pas seulement les WebSockets
  • Considérez la gestion des timeouts lors de la parallélisation - utilisez Future.wait avec gestion d'erreurs
  • Pour les services critiques, utilisez un écran de chargement plutôt que de bloquer le thread principal
  • Profilez avec Flutter DevTools Timeline pour identifier d'autres goulots d'étranglement au démarrage
  • Rappelez-vous que Future.wait() échoue rapidement par défaut - utilisez try/catch dans le map si vous voulez un succès partiel

Motifs Connexes

  • Splash screen avec initialisation asynchrone
  • Riverpod AsyncNotifier pour l'état chargé paresseusement
  • Initialisation de service en arrière-plan après la première frame

Références

Skills similaires