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.
// 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
// 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.
// 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.
// 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
, ouReact.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.
Navigation Sans Ralentissements
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é.
// 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.
# 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.
// 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.
// 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.
# 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
// 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.