Créer un lecteur PDF résistant aux pannes pour de grands documents dans React Native
Apprenez une architecture permettant de générer des PDF massifs de plusieurs milliers de pages dans React Native, avec des arbres de signets différés, une fonction de recherche et une navigation fiable sur les appareils à faibles performances.
Sources: Rendering and Navigating Large PDFs in React Native Without Crashing the App
Il est très simple d’afficher un PDF de cinq pages à l’écran dans une application React Native. En revanche, un document s’étendant sur des centaines ou des milliers de pages, comportant des signets profondément imbriqués et des liens entre sections internes représente un défi complètement différent — un défi qui peut rapidement entraîner des lenteurs, une absence de réactivité ou même des plantages sur du matériel moins puissant. Ce guide présente une architecture conçue pour relever directement ce défi : afficher efficacement de gros PDF, sauter vers des pages arbitraires, naviguer et rechercher dans des arbres de signets imbriqués, ainsi que maintenir une expérience stable même sur des appareils à ressources limitées.
Dans un cas de test, un PDF comptait plus de 1 900 pages et incluait un arbre de signets avec de nombreux niveaux d’imbrication. Tenter d’afficher tout cet arbre en même temps exerçait une pression considérable sur la mémoire. Les sections suivantes décrivent l’approche qui a permis de résoudre ces problèmes.
Ce que react-native-pdf vous offre — et ne vous offre pas
react-native-pdf, car cette limite influence le reste de la conception :
- Grâce à sa fonction de rappel
onLoadComplete, il offre bien des informations brutes sur les marques-pages (via un paramètretableContents) sur iOS et Android — mais pas sur Windows, et certainement pas sous une forme que l’on pourrait afficher directement à l’écran. Il s’agit de données brutes et non structurées qui nécessitent encore une analyse et un affichage de votre côté. - Il offre bien une fonction de rappel générique pour les liens (
onPressLink), mais cette fonction ne permet pas de distinguer un saut interne au sein du document d’une URL externe, et elle ne fournit pas de manière fiable le numéro de page cible pour les liens internes.
Compte tenu de cela, la solution pratique pour disposer de données fiables de favoris et de liens multiplateformes consiste à les extraire de manière indépendante plutôt que de compter sur la sortie brute des appels de fonction du rendeur — c’est précisément le rôle joué par pdf-lib, dont nous parlerons ensuite. Cette architecture ne vise pas à rechercher du texte à l’intérieur du contenu PDF lui-même ; elle s’attaque plutôt à la recherche des titres des favoris, une tâche considérablement plus simple qui peut être entièrement réalisée à l’aide d’outils open source.
Sélection des packages
La configuration repose sur trois packages, chacun étant chargé d’une tâche spécifique :
react-native-pdf gère l’affichage en utilisant le moteur PDF natif de la plateforme au lieu de convertir les pages en images basées sur JavaScript — c’est cette approche native qui permet de maintenir une consommation mémoire raisonnable lors du traitement d’un fichier volumineux. Il est chargé d’afficher les pages, de prendre en charge le zoom, de déclencher des événements de changement de page et de vous permettre de sauter vers une page de manière programmée.
react-native-blob-util est une dépendance dont react-native-pdf a lui-même besoin pour accéder aux fichiers — il s’occupe du téléchargement, du stockage en cache et de la lecture du PDF affiché. Ce n’est pas un ajout optionnel ; c’est une exigence intégrée au fonctionnement interne de react-native-pdf.
pdf-lib comble le manque mentionné précédemment. Plutôt que de rendre quoi que ce soit, il permet de lire et de manipuler directement la structure interne d’un PDF : le catalogue du document, l’arborescence des pages, et — ce qui est crucial pour ce cas d’usage — le dictionnaire des schémas, où se trouvent réellement les signets à l’intérieur d’un PDF, ainsi que les objets d’annotation de liens attachés aux pages individuelles, où sont stockés les liens internes et externes. Comme react-native-pdf ne présente rien de tout cela de manière structurée ou fiable, pdf-lib est utilisé pour parcourir cette structure interne une seule fois et générer une représentation JSON propre de l’arborescence des signets — titre, page cible et enfants imbriqués — ainsi qu’un ensemble de destinations de liens résolues, tout cela indépendamment du comportement spécifique à la plateforme du rendeur natif. Cette étape d’extraction doit être exécutée une seule fois plutôt que
Remarque Expo :
react-native-pdfdépend de modules natifs, il ne fonctionnera donc pas à l’intérieur du client standard Expo Go. Une build de développement personnalisée ou un projet natif est nécessaire.
L’architecture
Le système est organisé en trois couches, chacune isolée du fonctionnement interne des autres :
- Visualiseur PDF — suit la page actuelle, le nombre total de pages, l’état de chargement et tout état d’erreur.
- Barre d’outils du lecteur — affiche les détails de la page actuelle et permet d’accéder au panneau des favoris.
- Panier des favoris — gère la zone de recherche, l’arbre des favoris extensible (créé à partir de données extraites via
pdf-lib) et communique une seule information : la page sélectionnée par l’utilisateur.
Le panneau des signets ne prend pas en compte la manière dont le PDF est lui-même affiché, et le lecteur de PDF ne connaît pas la façon dont la recherche de signets est mise en œuvre. Ce flux unidirectionnel — le panneau sélectionne une page, et l’écran indique au lecteur de naviguer vers elle — est ce qui empêche des actions telles que la saisie dans un champ de recherche ou l’expansion d’un nœud arborescent de déclencher une reprise coûteuse de l’affichage du composant PDF lui-même.
Étape 1 : Structurer les données des signets
Chaque signet que vous utilisez nécessite trois éléments : un titre, une page cible, et éventuellement un ensemble de signets enfants — une structure récursive simple qui correspond naturellement à ce que l’étape d’extraction de pdf-lib restitue. Le détail le plus important ici est que chaque signet doit disposer d’un identifiant stable et unique, et cet identifiant ne peut pas être simplement sa position dans l’array. Dès qu’un utilisateur commence à taper pour rechercher ou à ouvrir/fermer des branches, la position d’un signet donné dans la liste affichée changera. Si vous vous fiez à l’index de l’array comme identifiant, React perd la capacité de distinguer les différentes lignes, l’état d’expansion/fermeture est associé au mauvais nœud, et FlatList commence à réutiliser les lignes de manière incorrecte.
Étape 2 : Rechercher sans perdre le contexte
Puisqu’un signet correspondant est souvent niché à plusieurs niveaux dans l’arbre, votre logique de recherche doit parcourir la structure de manière récursive plutôt que de ne scanner que les titres du niveau supérieur. Tout aussi important : chaque fois qu’un nœud enfant correspond au terme de recherche, vous devez également conserver son parent dans les résultats, même si le titre du parent ne correspond à rien. En omettant cela, un résultat profondément niché apparaîtra sans indication de son emplacement réel dans le document.
Pour maintenir une bonne réactivité, enveloppez la logique de filtrage dans un mécanisme de mémorisation afin qu’elle ne soit exécutée à nouveau que lorsque la chaîne de recherche ou les données du signet concerné changent. Pour les arbres contenant un grand nombre d’entrées, ajoutez également un mécanisme de débouncing à l’entrée de texte, afin de ne pas réfiltrer toute la structure à chaque frappe.
Étape 3 : Aplatir uniquement les nœuds visibles
FlatList est l’outil idéal pour des raisons de performance, mais il attend un tableau plat et non une structure arborescente imbriquée. Il vous faut donc une étape qui parcourt l’arbre des favoris et génère un tableau aplati contenant uniquement les nœuds qui doivent être visibles actuellement — les enfants d’un nœud ne sont inclus que si ce dernier est actuellement déplié. Chaque élément aplati doit également comporter un nombre depth afin de savoir à quel niveau l’indentation visuelle doit être appliquée.
Cette étape est sans doute la décision la plus importante en termes de performance dans toute l’implémentation. Si vous aplatissiez tout l’arbre et que vous masquiez simplement les lignes appartenant aux branches pliées, FlatList conserverait malgré tout des milliers de lignes qui n’apparaissent jamais à l’écran. En ne flattant que ce qui est actuellement déplié, un arbre de favoris comptant des milliers d’entrées ne transmettra à FlatList que quelques dizaines de lignes à un moment donné.
Étape 4 : Créer le panneau des favoris
Mettez en œuvre le panneau en tant que modal superposé au lecteur PDF plutôt qu’en tant qu’écran qui le remplace, et restreignez sa hauteur à environ 80 % de l’écran affiché au lieu de le laisser occuper tout l’écran. Cela laisse une petite partie visible du PDF en dessous pour faciliter la lecture, et vous offre de l’espace pour un bouton de fermeture visible, sans que le panneau ne paraisse être un écran complètement distinct.
Dans le modal, vous aurez besoin : d’un champ de recherche relié à la logique de l’Étape 2 ; d’une FlatList qui affiche les lignes simplifiées générées à l’Étape 3, configurée avec des propriétés de fenêtrage — initialNumToRender, maxToRenderPerBatch, windowSize et removeClippedSubviews — afin que seules les lignes proches de la zone visible soient réellement affichées ; ainsi que d’un état vide en cas de résultat nul après recherche. Chaque ligne doit présenter deux cibles de clic distinctes : en cliquant sur le corps de la ligne, on sélectionne cette page et on ferme le panneau, tandis que en cliquant sur une petite icône d’expansion, on affiche ou cache ses éléments enfants, de sorte qu’ouvrir une branche ne déclenche pas accidentellement de navigation. Enveloppez chaque composant de ligne dans memo afin que l’expansion d’une branche n’oblige pas à un re-render de tout le reste de la liste. De plus, chaque fois que le champ de recherche n’est pas vide, expandez automatiquement toute branche correspondante.
Étape 5 : Créer l’écran du lecteur PDF
C’est sur cet écran que s’effectue le rendu réel, grâce au composant Pdf. Utilisez la fonction de rappel déclenchée lors du changement de page pour suivre la page actuelle et le nombre total de pages, affichez l’indicateur de chargement intégré à la bibliothèque pendant le chargement du document, et connectez-vous à la fonction de rappel en cas d’erreur afin que, en cas de problème, l’écran affiche un message clair indiquant « impossible d’ouvrir ce document » accompagné d’une option de tentative supplémentaire — ne laissez pas un indicateur de chargement tourner indéfiniment sans solution de secours.
Lorsque vous naviguez vers une page depuis un signet, limitez le numéro de la page cible afin qu’il se situe entre 1 et le nombre total de pages connu. Cela permet d’éviter que une destination endommagée ou obsolète — qu’elle provienne des données du signet elles-mêmes ou d’un lien interne résolu — tente d’envoyer l’utilisateur vers un numéro de page qui n’existe tout simplement pas. Il est également conseillé d’activer le cache pour la source PDF, afin qu’un document déjà téléchargé ne soit pas récupéré à chaque fois que cette page s’affiche.
En ce qui concerne le style visuel, celui-ci dépend naturellement du système de conception propre à votre application — mais sur le plan structurel, les éléments importants pour l’ergonomie sont de maintenir la hauteur du panneau autour de 80 %, d’offrir un moyen évident de le fermer, de prévoir une zone de clic suffisamment grande pour chaque ligne (en particulier son icône d’expansion), ainsi que de concevoir un état d’erreur calme et clairement identifiable.
Étape 6 : Extraire une seule fois, pas à chaque rendu
L’étape d’extraction avec pdf-lib décrite précédemment doit s’exécuter exactement une seule fois, en dehors du cycle de rendu — et non à l’intérieur d’un composant qui se rérendit à chaque fois que l’utilisateur effectue une recherche ou développe un sous-arbre. Cachez le JSON résultant plutôt que de le recalculer à plusieurs reprises, conservez uniquement les champs légers (ids, titres, numéros de page) dans l’état React, évitez absolument de stocker des données brutes en format PDF dans l’état, et nettoyez tous les ressources temporaires une fois que l’écran est démonté. Si votre backend peut effectuer cette étape d’extraction à la place, c’est généralement l’approche la plus propre — l’application mobile n’a alors besoin de recevoir qu’un petit envoi JSON.
Étape 7 : Préserver la position de l’utilisateur
La fermeture du panneau des favoris ou le basculement de l’appareil ne devraient pas réinitialiser le lecteur à la première page. Au minimum, il est nécessaire de conserver la page actuelle, la branche du favori récemment dépliée ainsi que l’offset de défilement du panneau. Stockez ces valeurs au niveau de l’écran parent ou de l’état de navigation, et non à l’intérieur du composant du panneau des favoris lui-même — ainsi elles restent disponibles même si le panneau est désactivé.
Étape 8 : Gérer les erreurs avec élégance
Prévoyez les échecs dans le monde réel, et pas seulement le scénario idéal : des URLs de fichiers manquants ou expirés, des documents corrompus ou protégés par mot de passe, des appareils avec peu d’espace de stockage, des connexions réseau interrompues pendant le téléchargement, des favoris pointant vers des pages invalides, ou des liens qui ne s’ouvrent tout simplement pas. Quoi qu’il arrive, évitez de laisser l’utilisateur fixer un indicateur en rotation qui ne se résout jamais. Si un document ne contient pas de favoris, indiquez-le directement — par exemple : « Ce document ne contient pas de favoris. » Si une recherche ne renvoie rien, affichez « Aucun favori trouvé. » Un état vide ou d’erreur bien défini est bien préférable à un écran qui donne l’impression d’être bloqué.
Étape 9 : Tester les scénarios d’échec réels
N’validez pas votre implémentation en vous basant sur un seul PDF pratique. Étendez vos tests à ces différents aspects :
- Documents : fichiers ne contenant absolument aucun signet, fichiers comptant des centaines de pages, hiérarchies de signets profondément imbriquées, grandes images intégrées, protection par mot de passe, corruption intentionnelle, liens internes, liens externes, et titres contenant des caractères spéciaux
- Navigation : saut vers des signets au niveau racine ou profondément imbriqués, la première et la dernière page, un numéro de page hors plage, réouverture du panneau après l’avoir fermé, recherche d’une entrée imbriquée, et une recherche ne donnant aucun résultat
- Appareils : un téléphone Android à faible mémoire, un appareil phare récent, tant l’iPhone que l’iPad, orientation portrait et paysage, ainsi que les transitions entre arrière-plan et premier plan
Ici, les tests sur du matériel physique sont plus importants que dans la plupart des développements de fonctionnalités — un émulateur ne pourra jamais reproduire la pression sur la mémoire et les contraintes de stockage observées sur un véritable appareil économique.
Questions fréquentes
Pourquoi ne pas simplement afficher chaque page sous forme d’image ? Un moteur de rendu natif ne dessine que le contenu actuellement nécessaire à l’écran, au lieu de forcer React à gérer un composant d’image distinct pour chacune des milliers de pages.
Pourquoi utiliser pdf-lib au lieu des données de balises que react-native-pdf fournit déjà ? Parce que ces données intégrées ne sont pas traitées, se comportent de manière incohérente selon les plateformes (Windows ne renvoie rien du tout), et ne proposent aucun moyen fiable pour résoudre les destinations des liens internes. pdf-lib analyse directement l’arborescence du PDF ainsi que les structures d’annotation des liens en une seule passe, vous fournissant ainsi des données propres et prévisibles que vous contrôlez entièrement.
Une recherche de texte intégral à l’intérieur du document est-elle possible ? Pas avec un ensemble d’outils purement open source pour l’instant — cette fonctionnalité est actuellement réservée aux SDK commerciaux. La technique décrite ici concerne la recherche parmi les titres des balises, ce qui constitue un problème plus restreint que vous pouvez résoudre entièrement par vos propres moyens.
Faut-il que chaque branche de l’arbre des favoris s’affiche automatiquement ? Non — ne faites afficher que la branche que l’utilisateur ouvre activement. Sur les appareils à mémoire limitée, il est judicieux de restreindre l’interface afin qu’une seule branche au niveau racine puisse rester affichée à tout moment.
Tout cela garantit-il que l’application ne plantera jamais ? Aucune méthode ne peut faire cette promesse pour toutes les combinaisons possibles de documents et d’appareils. L’objectif est de réduire les risques : éviter le rendu prématuré, limiter l’état stocké, extraire les données une seule fois, virtualiser la liste des favoris, et gérer explicitement chaque mode de défaillance.
Considerations finales
Obtenir de bonnes performances ne consiste pas à choisir une seule bibliothèque — react-native-pdf s’occupe de l’affichage, pdf-lib fournit des informations fiables sur les signets et les liens, tandis que la véritable stabilité provient de la manière dont vous gérez l’état, la virtualisation et le traitement des erreurs liés à ces deux outils.
Règles pratiques à suivre :
- Exécutez l’extraction des signets et des liens une seule fois à l’aide de
pdf-lib, jamais dans le cadre d’un processus d’affichage. - N’affichez jamais l’ensemble de l’arborescence des signets en même temps — simplifiez-la et ne montrez que les parties dépliées.
- Soutenez la liste des signets avec un
FlatListvirtualisé. - Éloignez complètement les données brutes en format PDF de l’état React.
- Vérifiez la destination d’une page avant de naviguer vers elle.
- Conservez uniquement l’état minimal nécessaire pour restaurer la position de lectureur.