Kévin La Rosa

Senior Mobile Developer | iOS & React Native

Performance React Native : Leçons de Projets Réels

Apr 29, 20248 min read

Développer des applications React Native en production m'a appris que la performance n'est pas qu'une question de vitesse - c'est créer des expériences fluides. Voici les stratégies d'optimisation qui ont fait la plus grande différence dans mes projets.

Fuites Mémoire : Le Tueur Silencieux

Un des bugs les plus difficiles que j'ai débogué était une app qui crashait après 10 minutes d'utilisation. Le coupable ? Des fuites mémoire dues à des event listeners et timers non nettoyés. Maintenant j'implémente toujours des patterns de nettoyage appropriés.

typescript
// Pattern que j'utilise pour un nettoyage sûr
const useSafeCleanup = () => {
  useEffect(() => {
    const listener = eventEmitter.addListener('update', handleUpdate);
    const timeout = setTimeout(loadData, 5000);

    return () => {
      listener.remove();
      clearTimeout(timeout);
    };
  }, []);
};

LeakCanary sur Android et Instruments sur iOS sont devenus mes meilleurs amis pour traquer ces problèmes en production.

Faire Voler les Listes

Tout développeur React Native connaît la douleur du scrolling saccadé. Par essais et erreurs, j'ai découvert que la performance de FlatList n'est pas qu'une question de réglage - c'est trouver la bonne combinaison.

Les changements décisifs pour moi :

  • Définir des hauteurs d'items fixes avec getItemLayout pour éviter la phase de mesure
  • Ajuster windowSize selon la performance de l'appareil
  • Utiliser maxToRenderPerBatch pour contrôler les cycles de rendu
  • Implémenter removeClippedSubviews pour les longues listes
typescript
// Ma configuration FlatList de base
const PerformantList = () => {
  const renderItem = useCallback(({ item }) => (
    <ItemComponent data={item} />
  ), []);

  return (
    <FlatList
      data={listData}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      getItemLayout={(_, index) => ({
        length: 80,
        offset: 80 * index,
        index,
      })}
      windowSize={10}
      maxToRenderPerBatch={5}
      initialNumToRender={10}
    />
  );
};

FlashList de Shopify va encore plus loin avec le recyclage de vues - un game-changer pour les apps avec des listes massives.

Optimisation des Images

Les images peuvent faire ou défaire la performance de votre app. Les changements les plus impactants que j'ai faits :

**Format WebP** : Passer à WebP a réduit la taille des images de 30-40% comparé à JPEG/PNG, sans perte de qualité visible. La plupart des CDN supportent maintenant la conversion WebP automatique.

**Prefetching Stratégique** : Pour les images critiques (comme les bannières hero ou photos produits), le prefetching pendant que les utilisateurs naviguent améliore dramatiquement la performance perçue.

typescript
// Prefetch des images avant navigation
const prefetchImages = async (imageUrls: string[]) => {
  await Promise.all(imageUrls.map(url => Image.prefetch(url)));
};

// Dans le handler de navigation
const navigateToProduct = async (productId: string) => {
  const images = await getProductImages(productId);
  prefetchImages(images); // Commence le chargement immédiatement
  navigation.navigate('ProductDetail', { productId });
};

Pour la gestion avancée des images, j'ai eu beaucoup de succès avec react-native-nitro-image - conçu pour la performance dès le départ.

Mises à Jour Intelligentes des Composants

Comprendre quand et pourquoi les composants se re-render a transformé ma façon d'écrire du code React Native. Tous les composants n'ont pas besoin de mémoisation, mais savoir lesquels en ont besoin est crucial.

Ma règle : mémoiser quand vous avez des calculs coûteux, des composants visuels complexes, ou des items dans des listes. Le React DevTools Profiler aide à identifier les vrais coupables.

typescript
// Mémoisation stratégique
const DataCard = React.memo(({ info, onSelect }) => {
  return (
    <Pressable onPress={onSelect}>
      <ExpensiveVisualization data={info} />
    </Pressable>
  );
}, (prev, next) => {
  // Re-render seulement si les données ont vraiment changé
  return prev.info.id === next.info.id;
});

Le Futur : React Compiler

J'ai hâte de tester le React Compiler (anciennement React Forget) quand il sera disponible pour React Native. Il promet d'optimiser automatiquement les composants sans mémoisation manuelle.

Pourquoi c'est important :

  • Plus de boilerplate useMemo, useCallback, ou React.memo
  • Le compilateur détecte automatiquement ce qui nécessite une optimisation
  • Prévient les erreurs courantes comme les dépendances manquantes
  • Pourrait éliminer des catégories entières de problèmes de performance

En attendant, l'optimisation manuelle reste essentielle, mais le futur s'annonce beaucoup plus simple.

Passer de la navigation JavaScript à la navigation native stack a été le jour et la nuit. L'implémentation native gère automatiquement la mémoire en démontant les écrans invisibles.

La fonctionnalité freeze empêche les écrans cachés de se mettre à jour - exactement comme les apps natives iOS et Android. Cependant, certains écrans (comme les cartes ou caméras) bénéficient de rester montés pour un accès instantané.

typescript
// Activer les optimisations natives
import { enableFreeze } from 'react-native-screens';
enableFreeze(true);

// Native stack pour une meilleure gestion mémoire
const Stack = createNativeStackNavigator();

Vitesse de Lancement & Taille du Bundle

Les utilisateurs jugent les apps dans les premières secondes. Le moteur Hermes est devenu mon choix par défaut - il compile JavaScript à l'avance, réduisant drastiquement le surcoût au démarrage.

Analyser la Taille du Bundle

expo-atlas a été inestimable pour comprendre ce qui gonfle l'app. Il visualise votre bundle et montre exactement quelles dépendances prennent de la place.

bash
# Générer l'analyse du bundle
npx expo-atlas build/output/index.html

Coupables courants que j'ai trouvés :

  • Moment.js → remplacé par date-fns (économisé 200KB)
  • Import complet de Lodash → fonctions cherry-pickées (économisé 150KB)
  • Dépendances dupliquées → dédupliquées avec yarn resolutions

Stratégie de Code Splitting

Combiné avec l'analyse du bundle, j'ai vu les temps de démarrage chuter de 70%. L'astuce est d'identifier quels écrans sont vraiment nécessaires au lancement versus ce qui peut charger à la demande.

typescript
// Lazy loading des écrans non critiques
const ProfileScreen = React.lazy(() => import('./screens/Profile'));
const SettingsScreen = React.lazy(() => import('./screens/Settings'));

const App = () => (
  <Suspense fallback={<LoadingView />}>
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Profile" component={ProfileScreen} />
      <Stack.Screen name="Settings" component={SettingsScreen} />
    </Stack.Navigator>
  </Suspense>
);

Tests de Performance & Monitoring

Tests de Régression de Performance Automatisés

Reassure est devenu essentiel dans mon pipeline CI/CD. Il mesure la performance de rendu des composants et détecte les régressions avant qu'elles n'atteignent la production.

typescript
// Test de performance avec Reassure
import { measurePerformance } from 'reassure';

test('Performance ProductList', async () => {
  const scenario = async () => {
    render(<ProductList items={mockProducts} />);
  };

  await measurePerformance(scenario, {
    runs: 10,
    warmupRuns: 3,
  });
});

// Les résultats montrent le nombre de renders et la durée
// ✅ ProductList: 12ms (±2ms) avec 3 renders

Ce que j'aime avec Reassure :

  • S'exécute en CI pour détecter les régressions de performance
  • Compare avec les mesures de base
  • Montre les changements du nombre de renders (souvent plus important que le timing)

Tests E2E de Performance

Flashlight adopte une approche différente - il mesure la performance pendant les flux utilisateur réels. Il exécute vos tests E2E tout en collectant des métriques de performance.

bash
# Exécuter Flashlight avec vos tests E2E
npx flashlight test --bundleId com.yourapp \
  --testCommand "yarn e2e:test" \
  --duration 10000

# Génère un rapport de performance détaillé
# - Timeline FPS
# - Utilisation CPU
# - Consommation mémoire
# - Activité du thread JS

La combinaison de Reassure (niveau composant) et Flashlight (niveau E2E) donne une couverture complète de la performance.

Passer au Natif Quand Nécessaire

Parfois JavaScript n'est pas assez rapide. N'ayez pas peur de descendre au code natif quand la performance l'exige.

**Expo Modules** : Excellent pour créer des vues natives avec Swift/Kotlin. Parfait quand vous avez besoin de composants UI spécifiques à la plateforme.

**Nitro Modules** : Va encore plus loin avec C++ et JSI pour des appels synchrones. Idéal quand chaque milliseconde compte.

Les deux rendent le développement natif accessible sans sacrifier la performance.

Tester sur de Vrais Appareils

La plus grande révélation dans mon parcours React Native a été de tester sur de vieux appareils bas de gamme. Un téléphone Android de 2017 avec 2GB de RAM révèle des problèmes de performance qu'un iPhone moderne cache complètement.

Outils de Débogage Essentiels

**iOS : Instruments**

  • CPU Profiler montre exactement quelles fonctions consomment le plus de temps
  • Memory Graph debugger révèle les cycles de rétention et fuites
  • Core Animation suit les chutes de FPS et problèmes de rendu

**Android : Android Studio Profiler**

  • Systrace visualise l'activité des threads et les frames perdues
  • Memory Profiler suit les allocations et le garbage collection
  • Network Profiler montre le timing des requêtes et tailles des payloads
typescript
// Marqueur de performance simple pour le débogage
const perfMark = (label: string) => {
  if (__DEV__) {
    performance.mark(label);
    console.log(`[PERF] ${label}: ${performance.now()}ms`);
  }
};

Leçons Clés des Tests sur Appareils

**Tester sur du Matériel Contraint** Les téléphones budget exposent ce que les appareils premium cachent. Si votre app tourne bien sur un appareil avec 2GB de RAM et un processeur lent, elle volera sur du matériel moderne.

**Surveiller les Vraies Métriques**

  • Chutes de FPS sous 60 = saccades visibles
  • Mémoire qui augmente avec le temps = fuite potentielle
  • Démarrage supérieur à 3 secondes = les utilisateurs abandonneront

**Monitoring en Production** Des outils comme Sentry et Firebase Performance montrent comment les vrais utilisateurs expérimentent votre app. Les données surprennent souvent - ce qui fonctionne au bureau peut échouer dans un métro bondé avec un réseau instable.

Pensées Finales

La leçon la plus précieuse ? Testez tôt et souvent sur les pires appareils que vous pouvez trouver. Un téléphone Android avec 2GB de RAM de 2017 vous apprendra plus sur la performance que n'importe quel profiler.

Souvenez-vous : vos utilisateurs n'utilisent pas votre machine de développement. Ils sont dans des métros bondés avec des connexions instables, utilisant des téléphones avec des dizaines d'apps qui se battent pour la mémoire. Développez pour eux, pas pour votre environnement de test.