Jean Desauw
Retour au blog
React NativeDevelopment

Les animations React Native avec Reanimated en production : ce qu'on ne te dit pas

JDJean Desauw
6 min de lecture
Les animations React Native avec Reanimated en production : ce qu'on ne te dit pas

Trois mois après avoir mis en production des animations dans une app de pratique musicale, j'ai commencé à recevoir des bug reports qui n'avaient aucun sens. Une barre de progression bloquée à 0. Un visualiseur de tempo figé en plein beat. Pas de crash logs. Pas d'erreurs JavaScript. L'animation avait juste... arrêté de répondre.

Le coupable n'était pas ma logique. C'était un simple object literal que j'avais passé à un worklet.

C'est ça, Reanimated en production : les bugs ne s'annoncent pas. Ils s'accumulent silencieusement jusqu'à ce qu'un vrai utilisateur sur un Android milieu de gamme t'envoie une vidéo d'une UI cassée, et tu passes une journée à fixer du code qui semble parfaitement correct.

Voici ce que j'ai appris en livrant des animations comme cœur de l'UX, pas comme décoration, avec Reanimated 3 sur Expo.

Le piège de la copie d'objet (le bug le plus silencieux de la librairie)

Si tu passes un objet JavaScript brut dans un worklet (pas un useSharedValue, juste un objet classique), Reanimated le copie une fois vers le thread UI et le traite comme immuable. Toute mise à jour que tu fais de cet objet côté JS ensuite est silencieusement ignorée.

const config = { bpm: 120, signature: 4 };
 
const animateBar = useAnimatedStyle(() => {
  // config.bpm here is FROZEN at 120, forever
  return { width: withTiming(config.bpm * 2) };
});

L'app ne crash pas. L'animation utilise juste des données périmées. En développement, Reanimated lance un warning à ce sujet, mais seulement en dev, et seulement si tu fais attention à la console. En production, rien.

Le fix est évident une fois que tu le sais : tout ce qui change doit être un useSharedValue. Le piège est sournois parce que le code a l'air de devoir marcher, et il marche au premier render.

La doc de Software Mansion couvre ça dans leur guide de troubleshooting des worklets, mais c'est enterré après le happy path du getting-started. Je l'ai trouvé par accident en debuguant, pas en lisant à l'avance.

Le tree shaking tue les worklets sur les builds prod Android

Celui-ci nous a frappés fort. Les animations marchaient parfaitement en développement, marchaient dans Expo Go, marchaient dans TestFlight. Build de production Android : crash instantané sur n'importe quel écran avec une animation.

Le bug est documenté dans l'issue GitHub #8752 (ouverte au moment où j'écris). Si ta config Metro ou babel a un tree shaking agressif activé, le bundler peut éliminer le code d'initialisation du runtime natif des Worklets. Reanimated s'attend à ce que le module natif soit initialisé avant qu'un worklet ne tourne. Si le tree shaking supprime cet appel de bootstrap, tu obtiens un crash sans message d'erreur exploitable.

Le workaround : dis explicitement à ton bundler de traiter les points d'entrée de Reanimated comme ayant des side effects. Dans ton babel.config.js :

plugins: [
  'react-native-reanimated/plugin', // this must be LAST
]

Vérifie que ta config Metro ne prune pas l'init natif. C'est spécifique à Android parce qu'iOS et Hermes gèrent le chargement des modules différemment. C'est le genre de bug qui finira par te convaincre qu'Android te déteste personnellement.

La New Architecture n'est pas une mise à niveau d'animation gratuite

Tout le monde pousse la New Architecture. Expo SDK 53+ l'active par défaut. Le discours est : plus rapide, plus moderne, meilleure interop. Pour la plupart des choses, c'est vrai.

Pour Reanimated spécifiquement, c'est plus nuancé.

Voilà ce qui piège les gens : Reanimated utilise JSI, la JavaScript Interface qui contourne le bridge, depuis 2020. C'est la raison pour laquelle il peut driver des animations à 60fps entièrement sur le thread UI. La New Architecture n'a pas donné à Reanimated son modèle de performance. Il en avait déjà un.

Ce que la New Architecture change, c'est la façon dont Fabric (le nouveau renderer) interagit avec les styles animés que Reanimated produit. Et la doc de performance de Software Mansion elle-même note explicitement des régressions après l'avoir activée sur Expo SDK 53+.

Mon approche : avant de migrer une app avec des animations complexes, benchmark tes chemins d'animation critiques. Fais-les tourner sur un Android milieu de gamme (pas un Pixel 8 Pro, un appareil de 2021), mesure les framerates avant et après. Ne suppose pas que la mise à niveau est un gain. Elle peut être neutre, elle peut régresser. Vérifie.

Expo Go est toxique pour le développement d'animations non triviales

Utilise un development build. Toujours. Dès le premier composant qui utilise useSharedValue.

Expo Go embarque une version figée de Reanimated liée à la version du SDK. Si ton projet utilise une version différente (ce qui arrivera, parce que Reanimated sort plus vite que les cycles du SDK Expo), tu développes contre la mauvaise couche native. Des bugs que tu ne peux pas reproduire dans Expo Go atteindront la production. Des bugs que tu peux reproduire dans Expo Go pourraient ne pas exister dans ton build réel.

EAS Build pour les clients de développement existe précisément pour ça. C'est plus long à mettre en place initialement mais ça sauve des jours de debugging mal attribué. L'équipe Expo le sait. Ils ne peuvent juste pas forcer tout le monde à l'adopter.

La directive 'worklet' manquante et le fallback silencieux vers le thread JS

C'est subtil et ça m'a déjà eu. Si une fonction à l'intérieur d'un worklet appelle une autre fonction qui n'est pas marquée avec 'worklet', Reanimated ne crash pas. Il bascule sur l'exécution de cette fonction sur le thread JS au lieu du thread UI.

Ton animation tourne toujours. Mais maintenant une partie passe par le bridge (ou la message queue en New Arch), et tu perds la garantie 60fps pour ces frames. La dégradation est progressive. Tu la remarques sur les appareils plus lents comme du jank qui apparaît sous charge mais pas en dev.

function calculatePosition(progress) {
  // Missing 'worklet' directive
  return progress * screenWidth;
}
 
const animatedStyle = useAnimatedStyle(() => {
  // This call is silently demoted to JS thread
  const pos = calculatePosition(progress.value);
  return { transform: [{ translateX: pos }] };
});

Ajoute 'worklet'; comme première ligne de toute fonction appelée depuis des styles animés, des gesture handlers, ou d'autres worklets. Le plugin ESLint de Reanimated attrape la plupart de ça à l'écriture. Active-le.

Le cycle de vie des SharedValue dans les composants longue durée

Dans une app où les animations tournent en continu (un métronome, un visualiseur de playback, un waveform en temps réel), les SharedValues créés dans des hooks nécessitent une gestion minutieuse du cycle de vie.

Les SharedValues survivent aux re-renders, ce que tu veux pour une animation fluide. Mais si le composant qui les a créés s'unmount et se remount via la navigation ou du rendu conditionnel, tu peux te retrouver avec plusieurs instances de SharedValue qui pilotent le même élément animé, ou avec des callbacks useAnimatedReaction qui se déclenchent encore sur des composants unmountés.

Le pattern qui marche : crée les SharedValues dans des hooks stables (useMemo avec des deps [] pour les valeurs vraiment statiques, ou au niveau du module pour l'état global d'animation). Annule les animations explicitement dans les cleanup functions. Si tu utilises des callbacks runOnJS qui mettent à jour du state React, protège-toi contre les updates sur composants unmountés.

Rien de tout ça n'est propre à Reanimated. C'est de la gestion de cycle de vie React appliquée à l'état d'animation. Les failure modes sont plus vicieux parce qu'ils se manifestent en glitches visuels difficiles à reproduire.

Ce que Reanimated en production exige vraiment

La librairie est excellente. Software Mansion livre vite et la surface de l'API est bien pensée. Mais Reanimated opère à l'intersection de trois runtimes (le thread JS, le thread UI, et la couche native), et les bugs à ces frontières sont subtils par nature.

La discipline : traite chaque frontière de worklet comme un point de défaillance potentiel, pin tes dépendances natives dur, ne teste jamais avec Expo Go, et benchmark avant de migrer d'architecture. Les bugs d'animation sont les pires. Ils sont visuels, visibles par l'utilisateur, et souvent impossibles à reproduire en dev.

Si tu construis quelque chose de complexe en React Native et que tu veux un deuxième regard sur l'architecture, va voir mon portfolio ou contacte-moi directement.

Premier chapitre gratuit

Apprenez le workflow agentic coding que j'utilise en production

Comment je structure mes repos, gère le contexte, et fais tourner des agents en production. Écrit pour que vous puissiez faire pareil.