La gestion des images dans votre application en React Native

Si votre application affiche un nombre non négligeable d’images provenant de sources distantes, vous allez devoir vous pencher sur la question de la gestion de ces images.

En effet, les images sont une part importante de votre application, et vous souhaitez avoir la meilleur expérience pour vos utilisateurs. Dans un monde idéal, voici notre liste de souhaits pour une gestion d’image :

  • Avoir une image par défaut en attendant que l’image soit chargée.
  • Avoir une vision du chargement de l’image en cours.
  • Avoir une gestion des erreurs de chargement.
  • Avoir une bonne gestion du cache pour une utilisation de l’application offline de meilleure qualité.

Plusieurs solution existent pour la gestion des images, à commencer par la bibliothèque Image proposée par React Native. Commençons donc par tester cette solution.

Image de React Native

J’ai créé une application contenant une grille de photos qui sont stockées sur une serveur personnel qui n’est pas optimisé pour la gestion des médias. Nous allons donc avoir une vision où l’application va devoir combler — dans certain cas — les limitations du serveur.

Je vais demander l’affichage de 30 images dans une Flatlist à 4 colonnes, avec une cellule très simple codée ainsi

imageCell = ({item, index}) => {
const imageMargin = 10;
const imageSize = ((Dimensions.get('screen').width-5*imageMargin)/4);
return (
<View style={{width:imageSize, height:imageSize, marginRight:imageMargin, marginBottom:imageMargin}}>
<Image style={{width:imageSize, height:imageSize}} source={{uri:item}} />
</View>
)
}

Le résultat fonctionne mais il manque beaucoup d’information sur le statut de l’image : est-elle en cours de chargement ? si le carré reste blanc, est-ce normal ?

Première étape très simple

Il peut être intéressant de mettre en place un placeholder indiquant les cellules dans lesquelles une image doit se charger. Pour cela, il existe 2 façons de faire :

  • Si vous n’avez pas d’image par défaut, vous pouvez tout simplement ajouter une couleur de fond à votre images.
  • Si vous avez une image par défaut (dans mon exemple j’ai trouvé celle-ci http://www.bagherra.eu/accueil/orionthemes-placeholder-image/), vous pouvez la stocker dans votre application est l’afficher en lieu et place de votre image tant que celle-ci n’a pas correctement fini de charger grace à l’attribut defaultSource.

Voici le code de ma cellule avec ce dernier attribut

imageCell = ({ item, index }) => {
const imageMargin = 10;
const imageSize = ((Dimensions.get(‘screen’).width — 5 * imageMargin) / 4);
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<Image
style={{ width: imageSize, height: imageSize, backgroundColor: ‘#EFEFEF’ }}
defaultSource={require(‘./placeholder.png’)}
source={{ uri: item }} />
</View>
)
}
On peut voir à gauche sur iOS que le placeholder fonctionne bien, alors qu’à droite sur Android l’image ne s’affiche pas. On a donc recourt à la couleur de fond.

Notre première condition d’avoir une image par défaut est remplie sur iOS mais sur Android cela ne fonctionne pas.

La seconde condition est de voir le chargement. Pour cela Image a un attribut loadingIndicatorSource qui semble remplir ce rôle, si on le juge sur son nom et sa description dans le documentation. Mais je n’ai réussi à le faire fonctionner ni sur iOS ni sur Android (il est en plus impossible de le faire fonctionner en même temps que defaultSource).

On pourrait utiliser les attributs onLoad, onLoadStart, onLoadEnd, etc. de la bibliothèque Image pour gérer la progression, mais cela rajouterai de la lourdeur au processus. Ce qui est important est surtout de montrer que le chargement de l’image est fini. Avec la mise en place actuelle, nous savons que l’image est correctement chargée, tout simplement quand elle s’affiche. Par contre s’il y a un échec, nous avons la même image (ou fond d’écran) que lors du chargement, donc nous restons dans le doute : l’image est-elle en train de charger ou est-elle tombé en échec ?

Pour cela nous allons utiliser la méthode onError, et lui envoyer l’URL en échec. Nous allons stocker chaque URL en échec dans un tableau failed. Nous allons demander à la Flatlist de se recharger si ce tableau est modifié (via l’attribut extraData). Enfin dans la cellule, nous allons regarder si l’URL de notre image est contenue dans le tableau failed. Si oui, nous affichons une image “symbolisant” l’échec.

imageCell = ({ item, index }) => {
const imageMargin = 10;
const imageSize = ((Dimensions.get(‘screen’).width — 5 * imageMargin) / 4);
if (this.state.failed.includes(item)) {
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<Image
style={{ width: imageSize, height: imageSize, backgroundColor: ‘#EFEFEF’ }}
source={require(‘./unavailable.jpg’)} />
</View>
)
}
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<Image
style={{ width: imageSize, height: imageSize, backgroundColor: ‘#EFEFEF’ }}
defaultSource={require(‘./placeholder.png’)}
onError={() => this.onError(item)}
source={{ uri: item }} />
</View>
)
}
onError = (item) => {
let { failed } = this.state;
failed.push(item);
this.setState({failed});
}
L’image est ici différente en cas d’échec

Maintenant que nous avons un point sur les statuts, il faut tester la gestion hors ligne. Toutes mes photos sont chargées. Je vais couper le courant et relancer l’application pour voir le comportement.

Gestion de base du cache

Nous pouvons donc voir, qu’alors que 29 images avaient déjà été chargées, seules 2 s’affichent quand on est hors-ligne. C’est donc très décevant. Quand on recherche ce que la documentation de React Native nous propose en gestion de cache, on peut voir qu’elle nous recommande d’utiliser une bibliothèque react-native-fast-image. Il est donc évident que la bibliothèque Image ne nous conviendra pas, pour une gestion efficace des images.

react-native-fast-image de DylanVann

Nous allons donc mettre en place react-native-fast-image de DylanVann.

L’installation est très simple.

yarn add react-native-fast-image
cd ios && pod install && cd ..

Nous importons notre nouvelle bibliothèque dans notre page, et nous allons modifier notre cellule pour qu’elle utilise FastImage.

import FastImage from ‘react-native-fast-image’;
[…]
imageCell = ({ item, index }) => {
const imageMargin = 10;
const imageSize = ((Dimensions.get(‘screen’).width — 5 * imageMargin) / 4);
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<FastImage
style={{ width: imageSize, height: imageSize }}
source={{ uri: item }} />
</View>
)
}
Utilisation de react-native-fast-image

FastImage ne propose pas de définir une image par défaut, ni une ressource à afficher pendant le chargement. Nous devons donc nous contenter d’une couleur de fond.

FastImage propose les mêmes méthodes qu’Image. Nous pouvons donc déjà mettre en place le onError comme pour la bibliothèque Image. (Je continue d’ailleurs à utiliser d’ailleurs la bibliothèque Image de React Native pour afficher l’image d’erreur, car FastImage n’a pas de valeur ajoutée ici)

imageCell = ({ item, index }) => {
const imageMargin = 10;
const imageSize = ((Dimensions.get(‘screen’).width — 5 * imageMargin) / 4);
if (this.state.failed.includes(item)) {
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<Image
style={{ width: imageSize, height: imageSize, backgroundColor: ‘#EFEFEF’ }}
source={require(‘./unavailable.jpg’)} />
</View>
)
}
return (
<View style={{ width: imageSize, height: imageSize, marginRight: imageMargin, marginBottom: imageMargin }}>
<FastImage
style={{ width: imageSize, height: imageSize, backgroundColor: ‘#EFEFEF’ }}
onError={() => this.onError(item)}
source={{ uri: item }} />
</View>
)
}

Enfin, il nous faut tester la gestion du cache pour laquelle cette bibliothèque est réputée. Après avoir chargé une première fois les images, je vais donc simuler la relance de l’app sans connexion, et voir le comportement de celle-ci.

Gestion hors ligne des images

On peut donc valider la très bonne gestion du cache de la bibliothèque.

La bibliothèque Image de React Native a donc la possibilité de définir une image par défaut, mais a une très mauvaise gestion du cache. La bibliothèque FastImage, elle n’a pas d’image par défaut mais une très bonne gestion du cache. Ce dernier point étant — de mon point de vue — bien plus important pour l’expérience utilisateur, je pense qu’il est utile de faire appel à cette bibliothèque pour une vrai bonne gestion des images dans son app. (La version 8 ayant de plus mis fin aux problèmes de fuites mémoire de la version 7).

De plus il est facile de gérer le manque d’image par défaut, et d’indicateur de progression grâce aux méthodes onLoadStart, onProgress et onLoadEnd.

Pour finaliser notre comparatif, nous allons jeter un oeil rapide aux performances de ces deux bibliothèques. Pour cela je vais modifier un peu la démo, en chargeant 330 images au lieu de 30. Je vais observer parallèlement la réaction de l’app en terme de fluidité, ainsi que la consommation mémoire grâce aux outils de Xcode.

Les résultats ci-dessous exposent la différence entre les 2 gestions de mémoire (La gestion de la Flatlist n’est pas optimisée. J’ai souhaité montrer ici des résultats avec un code basique).

Je suis monté à plus de 3,27Go de consommation de mémoire en utilisant Image de React , qui est resté à ce pic une fois l’ensemble des photos chargées. Le chargement des images a de plus gelé mon écran, et je n’ai pas pu scroller pendant quelques secondes avant que le premier lot d’images soit chargé.

Avec Image de React Native
Avec FastImage

Le même code avec FastImage a fait monté la consommation mémoire a un pic de 220Mo, et j’ai pu scroller sans problème dans ma vue pendant que les images étaient en train de charger.

Dans le développement de mes applications, j’ai tendance à limiter l’appel à des bibliothèques extérieures. En effet l’ajout trop systématiques de bibliothèques va alourdir l’app, et complexifier sa maintenance. Vous devenez de plus dépendant d’un ensemble de développeurs pour les montées de version. Dans le cas présent, FastImage représente une réelle valeur ajouté à votre application. Cela ne vous empêche pas d’être prudent et de tester les montées de version des vos bibliothèques avant de les mettre en prod. Dans la version 7 de FastImage, une fuite mémoire faisait crasher mes apps (la consommation mémoire pouvait atteindre plus de 4Go, et continuait à grimper même après le chargement des images).

Show Runner de projets mobiles. Développeur React Native et passionné par les challenges du monde mobile.

Show Runner de projets mobiles. Développeur React Native et passionné par les challenges du monde mobile.