Startseite / Artikel

Erstellung eines crashsicheren PDF-Lesers für große Dokumente in React Native

Erlernen Sie eine Architektur zum Rendern riesiger, mehrtausendseitiger PDF-Dateien in React Native – mit verzögerter Anzeige von Lesezeichenstrukturen, Suchfunktion sowie stabiler Navigation auf Geräten mit geringer Leistung.

Verlag: REACTAPP.TOP12 Min. Lesezeit2758 Wörter

Quellen: Rendering and Navigating Large PDFs in React Native Without Crashing the App

Ein fünfseitiges PDF ist in einer React Native-App problemlos auf dem Bildschirm anzuzeigen. Ein Dokument mit Hunderten oder Tausenden von Seiten, tief verschachtelten Lesezeichen sowie internen Verlinkungen zwischen Abschnitten stellt hingegen eine völlig andere Herausforderung dar – es kann bei weniger leistungsstarken Geräten schnell träge werden, unresponsiv sein oder sogar häufig abstürzen. Dieser Leitfaden zeigt eine Architektur, die diese Herausforderung direkt angeht: die effiziente Darstellung großer PDFs, das Springen zu beliebigen Seiten, das Durchblättern und Suchen in verschachtelten Lesezeichentrees sowie die Aufrechterhaltung einer stabilen Benutzererfahrung auch auf eingeschränkten Geräten.

In einem Testfall umfasste ein PDF über 1.900 Seiten sowie einen Lesezeichentree mit vielen Verschachtelungsebenen. Der Versuch, den gesamten Tree auf einmal anzuzeigen, führte zu erheblichem Speicherdruck. Die folgenden Abschnitte beschreiben den Ansatz, mit dem diese Probleme gelöst wurden.

Was react-native-pdf Ihnen bietet – und was nicht

Bevor man sich auf zusätzliche Bibliotheken entscheidet, ist es hilfreich, genau zu wissen, wo die eingebauten Funktionen von react-native-pdf enden, denn diese Grenze bestimmt den weiteren Entwurf:

  • Durch seine onLoadComplete-Funktion stellt es auf iOS und Android tatsächlich Rohdaten zu den Buchmarken bereit (über einen tableContents-Parameter) – aber nicht auf Windows und definitiv nicht in einer Form, die man direkt auf dem Bildschirm anzeigen kann. Es handelt sich dabei um unstrukturierte Rohdaten, die auf der Seite des Benutzers noch verarbeitet und dargestellt werden müssen.
  • Es bietet zwar eine allgemeine Tap-Funktion für Links (onPressLink), doch diese kann keinen Unterschied zwischen einem internen Sprung innerhalb des Dokuments und einer externen URL erkennen, und sie liefert für interne Links auch nicht zuverlässig die Ziel-Seitennummer.
  • Es bietet keine eingebaute Volltextsuche innerhalb des Dokuments. Dies ist kein Mangel, der speziell dieser Bibliothek eigen ist – eine echte Textsuche direkt im Dokument fehlt derzeit im Bereich der Open-Source React Native PDF-Lösungen im Grunde gänzlich. Die wenigen Tools, die sie unterstützen, wobei Nutrient (früher PSPDFKit genannt) am bekanntesten ist, sind kommerzielle SDKs mit Lizenzkosten pro Plattform, die weit über das hinausgehen, was die meisten kleinen Teams oder persönlichen Projekte sich leisten können.
  • Angesichts dessen besteht der praktische Weg zu zuverlässigen, plattformübergreifenden Daten zu Bookmarks und Links darin, diese unabhängig abzurufen anstatt von der rohen Rückrufausgabe des Renderers abhängig zu sein – genau diese Rolle übernimmt pdf-lib, wie im Folgenden erläutert wird. Das Suchen von Text innerhalb des PDF-Inhalts selbst ist nicht das Ziel dieser Architektur; stattdessen wird das Suchen nach Bookmark-Überschriften angegangen, eine deutlich einfachere Aufgabe, die mit Open-Source-Tools vollständig umsetzbar ist.

    Auswahl der Pakete

    Die Einrichtung setzt drei Pakete voraus, von denen jedes für eine spezifische Aufgabe zuständig ist:

    react-native-pdf kümmert sich um die Darstellung, indem es den nativen PDF-Engine der Plattform verwendet anstelle von Seiten in auf JavaScript basierende Bilder umzuwandeln – dieser native Ansatz sorgt dafür, dass der Speicherverbrauch bei der Verarbeitung großer Dateien in Grenzen bleibt. Er ist dafür verantwortlich, die Seiten anzuzeigen, das Zoomen zu unterstützen, Ereignisse bei Seitenwechseln auszulösen und es zu ermöglichen, programmatisch auf eine Seite zu springen.

    react-native-blob-util ist eine Abhängigkeit, auf die react-native-pdf selbst für den Zugriff auf Dateien angewiesen ist – sie kümmert sich um das Herunterladen, Speichern im Cache und Lesen des angezeigten PDFs. Es handelt sich dabei nicht um ein optionales Zusatzmodul, sondern um eine Voraussetzung, die in der internen Funktionsweise von react-native-pdf integriert ist.

    pdf-lib schließt die zuvor beschriebene Lücke. Anstatt etwas darzustellen, ermöglicht es Ihnen, die interne Struktur eines PDFs direkt zu lesen und zu verarbeiten: den Dokumentkatalog, den Seitenbaum sowie – was für diesen Anwendungsfall entscheidend ist – das Outline-Dictionary, in dem die Buchmarken tatsächlich innerhalb eines PDFs gespeichert sind, zusammen mit den Link-Annotation-Objekten, die an einzelne Seiten angehängt sind und dort sowohl interne als auch externe Links speichern. Da react-native-pdf all dies nicht auf strukturierte oder zuverlässige Weise bereitstellt, wird pdf-lib verwendet, um diese interne Struktur einmal durchzusuchen und eine saubere JSON-Repräsentation des Buchmarkenbaums zu erzeugen – mit Titel, Zielseite und verschachtelten Kindern – sowie eine aufgelöste Liste der Link-Ziele, alles unabhängig vom plattformspezifischen Verhalten des nativen Renderers. Dieser Extraktionsschritt sollte nur einmal ausgeführt werden.

    In jeder Darstellung wird dies ausführlicher in Schritt 6 erläutert.

    Hinweis zu Expo: react-native-pdf ist von nativen Modulen abhängig, weshalb es nicht im Standard-Expo Go-Klienten laufen kann. Es ist ein benutzerdefinierter Entwicklungsbuild oder ein reines/natives Projekt erforderlich.

    Die Architektur

    Das System ist in drei Schichten gegliedert, wobei jede Schicht von den internen Abläufen der anderen isoliert ist:

    • PDF-Viewer – überwacht die aktuelle Seite, die Gesamtzahl der Seiten, den Ladezustand sowie eventuelle Fehlerzustände.
    • Lesezeichenleiste – zeigt Details zur aktuellen Seite an und gibt Zugriff auf das Lesezeichenpanel.
    • Lesezeichenpanel – verwalten die Sucheingabe, den erweiterbaren Lesezeichenbaum (der aus über pdf-lib extrahierten Daten erstellt wird) und geben lediglich eine Information zurück: welche Seite der Benutzer ausgewählt hat.

    Das Lesezeichen-Panel hat keine Kenntnis davon, wie der PDF selbst dargestellt wird, und der PDF-Viewer kennt nicht, wie die Suche nach Lesezeichen umgesetzt ist. Dieser einseitige Ablauf – das Panel wählt eine Seite aus, und der Bildschirm weist den Viewer an, dorthin zu navigieren – verhindert, dass Aktionen wie das Eingeben in ein Suchfeld oder das Erweitern eines Baumknotens zu einer aufwändigen Neudarstellung des PDF-Komponenten selbst führen.

    Schritt 1: Die Lesezeichendaten strukturieren

    Jeder Bookmark, mit dem Sie arbeiten, benötigt drei Dinge: einen Titel, eine Zielseite sowie optional eine Liste von Unter-Bookmarks – eine einfache rekursive Struktur, die sich nahtlos mit dem Ergebnis der pdf-lib-Extraktionsphase übereinstimmt. Entscheidend ist dabei, dass jeder Bookmark einen stabilen, eindeutigen Identifikator haben muss, wobei dieser nicht einfach seine Position in der Liste sein darf. Sobald ein Benutzer zu suchen beginnt oder Unterseiten öffnet und schließt, ändert sich die Position eines bestimmten Bookmarks in der angezeigten Liste. Verlassen Sie sich auf den Array-Index als Identifikator, verliert React die Fähigkeit, verschiedene Zeilen voneinander zu unterscheiden, der Zustand „Erweitern/Kompaktieren“ wird dem falschen Knoten zugeordnet, und FlatList beginnt, Zeilen falsch zu wiederverwenden.

    Schritt 2: Suchen ohne Kontextverlust

    Da ein passender Lesezeichen häufig mehrere Ebenen tief im Baumstruktur angeordnet ist, muss Ihre Suchlogik die Struktur rekursiv durchlaufen und nicht nur die Titel der obersten Ebene überprüfen. Ebenso wichtig: Sobald ein Kindknoten mit dem Suchbegriff übereinstimmt, müssen Sie auch seinen Elternknoten in den Ergebnissen berücksichtigen – selbst wenn der Titel des Elternknotens selbst nicht mit etwas übereinstimmt. Verpassen Sie das, und ein tief in der Struktur liegender Treffer erscheint ohne Anzeige dafür, wo er sich tatsächlich im Dokument befindet.

    Um dies reaktionsfähig zu halten, sollten Sie die Filterlogik in einen Memoisierungshook einbetten, sodass sie nur dann erneut ausgeführt wird, wenn sich der Suchbegriff oder die zugrunde liegenden Lesezeichendaten ändern. Bei Bäumen mit einer großen Anzahl von Einträgen sollten Sie außerdem eine Verzögerung bei der Eingabe von Text hinzufügen, damit die gesamte Struktur nicht bei jedem Tastendruck neu gefiltert wird.

    Schritt 3: Nur die sichtbaren Knoten vereinfachen

    FlatList ist aus Leistungsgründen das richtige Werkzeug, erwartet jedoch ein flaches Array und keine verschachtelte Baumstruktur. Daher benötigen Sie einen Durchlauf, der Ihren Favoritenbaum durchläuft und ein flaches Array erzeugt, das nur die Knoten enthält, die derzeit sichtbar sein sollten – die Kinder eines Knotens werden nur dann mit aufgenommen, wenn dieser Knoten derzeit ausgeklappt ist. Jeder geflachte Eintrag sollte außerdem eine depth-Zahl enthalten, damit Sie wissen, wie tief er visuell eingerückt werden soll.

    Dieser Schritt ist wohl die entscheidendste Leistungsentscheidung in der gesamten Implementierung. Wenn man den gesamten Baum vereinfachen und lediglich die Zeilen verbergen würde, die zu zusammengeklappten Zweigen gehören, würde FlatList weiterhin Tausende von Zeilen speichern, die niemals auf dem Bildschirm angezeigt werden. Indem man nur das Vereinfacht, was derzeit erweitert ist, kann ein Lesezeichenbaum mit Tausenden von Einträgen dem FlatList zu jedem Zeitpunkt nur ein paar Dutzend Zeilen übergeben.

    Schritt 4: Das Lesezeichen-Panel erstellen

    Implementieren Sie das Panel als Modales Fenster, das über dem PDF-Viewer angeordnet ist anstelle eines Fensters, das diesen ersetzt, und begrenzen Sie seine Höhe auf etwa 80 % der Anzeigefläche, anstatt es die gesamte Bildschirmeinheit einnehmen zu lassen. Dadurch bleibt ein sichtbarer Bereich des PDFs darunter für die Orientierung, und es entsteht Platz für eine deutlich sichtbare Schließen-Schaltfläche, anstatt dass das Panel wie ein völlig separates Fenster wirkt.

    Darin sollten Sie folgende Elemente haben: ein Suchfeld, das mit der Logik aus Schritt 2 verbunden ist; eine FlatList, die die in Schritt 3 erzeugten, vereinfachten Zeilen anzeigt und mit den Eigenschaften initialNumToRender, maxToRenderPerBatch, windowSize sowie removeClippedSubviews konfiguriert ist, sodass nur Zeilen in der sichtbaren Bereich tatsächlich angezeigt werden; sowie einen Ersatz-Zustand für den Fall, dass die Suche nichts ergibt. Jede Zeile sollte zwei separate Tap-Ziele bieten: Ein Tippen auf den Zeileninhalt wählt die entsprechende Seite aus und schließt das Panel, während ein Tippen auf ein kleines Erweiterungsicon die darin enthaltenen Elemente anzeigt – damit das Öffnen eines Unterbaums nicht versehentlich eine Navigation auslöst. Umhüllen Sie jeden Zeilenkomponente mit memo, damit das Erweitern eines Unterbaums nicht zu einer erneuten Darstellung des gesamten Lists führt. Zudem sollte bei nicht leeren Suchfeldern automatisch jeder Unterbaum erweitert werden.

    Damit die Benutzer nicht gezwungen sind, durch zusammengeklappte Knoten zu suchen, um genau das Ergebnis zu finden, nach dem sie gesucht haben.

    Schritt 5: Erstellen des PDF-Reader-Bildschirms

    Hier findet die eigentliche Darstellung statt, mithilfe des Pdf-Komponenten. Verwenden Sie den Callback für Änderungen der Seite, um die aktuelle Seite sowie die Gesamtzahl der Seiten zu verfolgen, den integrierten Ladeindikator der Bibliothek während des Ladens des Dokuments anzuzeigen, und binden Sie den Fehler-Callback ein, sodass im Falle eines Fehlers der Bildschirm auf eine klare Nachricht „Dieses Dokument konnte nicht geöffnet werden“ mit einer Wiederholungsoption umschaltet wird – lassen Sie den Ladeindikator nicht endlos laufen, ohne eine Alternative anzubieten.

    Jedes Mal, wenn Sie von einem Favoriten auf eine Seite navigieren, begrenzen Sie die Ziel-Seitenzahl so, dass sie zwischen 1 und der bekannten Gesamtanzahl an Seiten liegt. Dadurch wird verhindert, dass ein fehlerhafter oder veralteter Zielort – egal ob aus den Favoritendaten selbst oder aus einem aufgelösten internen Link – versucht, den Benutzer zu einer Seitenzahl zu leiten, die einfach nicht existiert. Es ist außerdem sinnvoll, das Caching für die PDF-Quelle zu aktivieren, damit ein bereits heruntergeladenes Dokument nicht jedes Mal neu heruntergeladen wird, wenn dieses Bildschirmelement angezeigt wird.

    Was die visuelle Gestaltung betrifft, hängt diese natürlich vom eigenen Design-System Ihrer Anwendung ab – doch strukturell sind für die Benutzerfreundlichkeit wichtig, das Panel auf eine Höhe von etwa 80 % zu begrenzen, eine klare Möglichkeit zum Schließen bereitzustellen, jedem Reiter (insbesondere seinem Erweiterungs-Icon) eine angemessene Berührungsoberfläche zu geben sowie einen ruhigen, eindeutig erkennbaren Fehlerzustand zu gestalten.

    Schritt 6: Extrahieren Sie einmal, nicht bei jeder Darstellung

    Der zuvor erläuterte Extraktionsvorgang mit pdf-lib sollte genau einmal ausgeführt werden, außerhalb des Darstellungszyklus – und nicht innerhalb einer Komponente, die jedes Mal neu gerendert wird, wenn der Benutzer sucht oder einen Zweig erweitert. Cachen Sie stattdessen die resultierende JSON-Datei als Lesezeichen, anstatt sie immer wieder neu zu berechnen. Behalten Sie in dem React-State nur die leichtgewichtigen Felder (IDs, Titel, Seitenzahlen) bei, vermeiden Sie es, rohe PDF-Byte-Daten im State zu speichern, und räumen Sie alle temporären Ressourcen auf, sobald der Bildschirm entmontiert wird. Falls Ihr Backend in der Lage ist, diesen Extraktionsvorgang durchzuführen, ist das in der Regel der sauberere Ansatz – die Mobile-App muss dann nur einen kleinen JSON-Datenstrom erhalten.

    Schritt 7: Die Position des Benutzers beibehalten

    Das Schließen des Buchmarkenpanels oder das Drehen des Geräts sollte den Reader nicht auf die erste Seite zurücksetzen. Mindestens müssen die aktuelle Seite, der kürzlich erweiterte Buchmarkenzweig sowie der Scroll-Offset des Panels beibehalten werden. Speichern Sie diese Werte auf Ebene des übergeordneten Bildschirms oder des Navigationszustands – und nicht innerhalb der Buchmarkenpanel-Komponente selbst – damit sie auch dann erhalten bleiben, wenn das Panel deaktiviert wird.

    Schritt 8: Fehler angemessen handhaben

    Planen Sie für tatsächliche Fehler, nicht nur für den idealen Ablauf: fehlende oder abgelaufene Datei-URLs, beschädigte oder mit Passwort geschützte Dokumente, Geräte mit knappem Speicherplatz, Netzwerkverbindungen, die während des Downloads abbrechen, Lesezeichen, die auf ungültige Seiten verweisen, oder Links, die einfach nicht öffnen. Egal was passiert – vermeiden Sie es, den Benutzer vor einem sich endlos drehenden Ladeindikator sitzen zu lassen. Wenn ein Dokument keine Lesezeichen enthält, teilen Sie das direkt mit – etwa mit „Dieses Dokument enthält keine Lesezeichen.“ Wenn eine Suche nichts ergibt, zeigen Sie „Keine Lesezeichen gefunden.“ Ein klar definiertes leeres oder fehlerhaftes Zustandsbild ist weitaus besser als ein Bildschirm, der nur so aussieht, als wäre er stecken geblieben.

    Schritt 9: Testen Sie die tatsächlichen Fehlerfälle

    Überprüfen Sie Ihre Implementierung nicht nur anhand eines einzelnen, praktischen PDFs. Erweitern Sie Ihren Test auf folgende Aspekte:

    • Dokumente: Dateien ohne jegliche Lesezeichen, Dateien mit Hunderten von Seiten, tief verschachtelte Hierarchien von Lesezeichen, große eingebettete Bilder, Passwortschutz, absichtliche Beschädigung der Dateien, interne Links, externe Links sowie Titel mit Sonderzeichen
    • Navigation: Das Springen auf Lesezeichen auf Wurdebene oder in tief verschachtelten Strukturen, zur allerersten und letzten Seite, auf eine außerhalb des Bereichs liegende Seitennummer, das Wiederöffnen des Panels nach seinem Schließen, die Suche nach einer verschachtelten Eintragung sowie eine Suche ohne Ergebnisse
    • Geräte: ein Android-Telefon mit geringer Speicherkapazität, ein neues Flaggschiff-Modell, sowohl iPhone als auch iPad, Portrait- und Landschaftsansicht sowie Übergänge zwischen Hintergrund und Vordergrund
  • Netzwerkbedingungen: Arbeiten offline mit einem bereits im Cache gespeicherten Dokument, Verlust der Verbindung während des Downloads, erneuter Versuch nach Wiederherstellung der Verbindung sowie eine abgelaufene oder nicht erreichbare URL eines entfernten Dateis.
  • Bei Tests auf physischer Hardware ist das hier wichtiger als bei den meisten Funktionsentwicklungen – ein Emulator kann einfach nicht den Speicherdruck und die Speicherbeschränkungen nachbilden, wie man sie an einem echten Budget-Gerät erlebt.

    Häufig gestellte Fragen

    Warum wird nicht einfach jede Seite als Bild gerendert? Ein natives Rendering-Engine zeichnet nur den derzeit auf dem Bildschirm benötigten Inhalt auf, anstatt React dazu zu zwingen, für jede der tausend Seiten einen separaten Bildkomponenten zu verwalten.

    Warum sollte man pdf-lib statt der von react-native-pdf bereits bereitgestellten Bookmark-Daten verwenden? Weil diese eingebauten Daten unverarbeitet sind, auf verschiedenen Plattformen inkonsistent funktionieren (Windows gibt überhaupt keine Daten zurück) und keinen zuverlässigen Weg bieten, um Ziele interner Links zu ermitteln. pdf-lib analysiert die Strukturen des PDF-Auszugs sowie der Link-Anmerkungen direkt in einem einzigen Durchlauf und liefert Ihnen saubere, vorhersehbare Daten, über die Sie vollständig die Kontrolle haben.

    Ist eine Volltextsuche innerhalb des Dokuments selbst möglich? Derzeit nicht mit reinen Open-Source-Toolchains – diese Funktion ist derzeit nur kommerziellen SDKs vorbehalten. Die hier beschriebene Methode umfasst die Suche nach Bookmark-Überschriften, was ein begrenzteres Problem ist, das Sie vollständig selbst lösen können.

    Soll sich jeder Zweig im Favoritenbaum automatisch ausklappen? Nein – nur der von dem Benutzer aktiv geöffnete Zweig soll sich ausklappen. Auf Geräten mit begrenzter Speicherkapazität lohnt es sich, die Benutzeroberfläche so einzuschränken, dass gleichzeitig nur ein Zweig auf Wurzelebene ausgeklappt bleiben kann.

    Garantiert das irgendetwas, dass die App niemals abstürzt? Keiner der Ansätze kann dieses Versprechen für jede mögliche Kombination aus Dokument und Gerät geben. Das Ziel ist es, das Risiko zu verringern: verzichten Sie auf vorzeitige Darstellung, halten Sie den gespeicherten Zustand minimal, extrahieren Sie Daten nur einmal, virtualisieren Sie die Favoritenliste und handhaben Sie explizit jeden möglichen Fehlerfall.

    Fazit

    Die richtige Leistung zu erzielen bedeutet nicht, nur eine Bibliothek auszuwählen – react-native-pdf übernimmt die Darstellung, pdf-lib liefert zuverlässige Informationen zu Lesezeichen und Links, während wahre Stabilität davon abhängt, wie Sie den Zustand, die Virtualisierung sowie das Fehlerbehandlungssystem in Bezug auf beide Bibliotheken verwalten.

    Die praktischen Richtlinien sind:

    1. Führen Sie die Extraktion von Lesezeichen und Links einmal mithilfe von pdf-lib durch, niemals als Teil des Darstellungsprozesses.
    2. Darstellen Sie niemals den gesamten Lesezeichenbaum auf einmal – strukturieren Sie ihn und zeigen Sie nur die erweiterten Abschnitte an.
    3. Stützen Sie die Liste der Lesezeichen durch eine virtualisierte FlatList.
    4. Lassen Sie Rohdaten des PDF-Bytes vollständig außerhalb des React-Zustands.
    5. Prüfen Sie das Ziel einer Seite vor dem Wechsel dorthin.
    6. Speichern Sie nur den minimalen Zustand, der benötigt wird, um die Position des Lesers wiederherzustellen.
  • Testen Sie mit tatsächlich großen Dokumenten auf wirklich begrenzten Geräten mit geringer Speicherkapazität.
  • Zeigen Sie stets einen klaren Fehler oder einen leeren Zustand an – lassen Sie niemals einen endlosen Ladeindikator auf dem Bildschirm.