Crear un lector de PDF a prueba de fallos para documentos grandes en React Native
Aprender una arquitectura para renderizar PDFs masivos de miles de páginas en React Native, con árboles de marcadores perezosos, búsqueda y navegación estable en dispositivos de bajo rendimiento.
Fuentes: Rendering and Navigating Large PDFs in React Native Without Crashing the App
Mostrar un PDF de cinco páginas en la pantalla de una aplicación React Native es algo sencillo. Sin embargo, un documento que abarca cientos o miles de páginas, cuenta con marcadores profundamente anidados y enlaces entre secciones internas representa un desafío completamente distinto: puede volverse lento, no responder o incluso colapsar fácilmente en hardware menos potente. Esta guía explica una arquitectura que aborda directamente ese problema: renderizar PDFs grandes de manera eficiente, saltar a páginas arbitrarias, navegar y buscar en árboles de marcadores anidados, y mantener la estabilidad de toda la experiencia incluso en dispositivos con recursos limitados.
En un caso de prueba, se utilizó un PDF con más de 1,900 páginas que incluía un árbol de marcadores con numerosos niveles de anidamiento. Intentar expandir todo ese árbol de una sola vez generó una gran carga en la memoria. Las secciones siguientes describen el enfoque que resolvió estos problemas.
Qué ofrece y qué no ofrece react-native-pdf
Antes de decidirse por bibliotecas complementarias, es útil saber con exactitud dónde terminan las capacidades integradas de react-native-pdf, ya que ese límite determina el resto del diseño:
- A través de su callback
onLoadComplete, sí expone información bruta de marcadores (a través del parámetrotableContents) en iOS y Android, pero no en Windows, y definitivamente no en un formato que se pueda mostrar directamente en la pantalla. Se trata de datos brutos y no estructurados que aún requieren procesamiento y renderizado por su parte. - Sí ofrece un callback genérico para hacer clic en enlaces (
onPressLink), pero este callback no puede distinguir entre un salto interno dentro del documento y una URL externa, ni proporciona de manera fiable el número de página de destino para los enlaces internos.
Dado esto, el enfoque práctico para obtener datos fiables de marcadores y enlaces multiplataforma es extraerlos de forma independiente en lugar de depender de la salida bruta del callback del renderizador, que es precisamente la función que desempeña pdf-lib, como se explicará a continuación. Esta arquitectura no intenta buscar texto dentro del propio contenido del PDF; en cambio, se ocupa de buscar títulos de marcadores, una tarea considerablemente más sencilla que se puede llevar a cabo completamente con herramientas de código abierto.
Elegir los paquetes
La configuración depende de tres paquetes, cada uno encargado de una tarea específica:
react-native-pdf se encarga de la renderización utilizando el motor PDF nativo de la plataforma en lugar de convertir las páginas en imágenes basadas en JavaScript; este enfoque nativo es lo que mantiene el consumo de memoria razonable al tratar con un archivo grande. Es responsable de mostrar las páginas, admitir el zoom, generar eventos de cambio de página y permitir saltar a una página de forma programática.
react-native-blob-util es una dependencia de la que depende react-native-pdf para acceder a los archivos; se ocupa de descargar, almacenar en caché y leer el PDF que se muestra. No es un complemento opcional, sino un requisito inherente al funcionamiento interno de react-native-pdf.
pdf-lib soluciona la brecha mencionada anteriormente. En lugar de renderizar nada, permite leer y manipular directamente la estructura interna de un PDF: el catálogo del documento, el árbol de páginas y, lo que es crucial para este caso de uso, el diccionario de esquema, donde se encuentran realmente los marcadores dentro de un PDF, junto con los objetos de anotación de enlaces asociados a cada página, donde se almacenan tanto los enlaces internos como externos. Dado que react-native-pdf no muestra nada de esto de manera estructurada ni fiable, se utiliza pdf-lib para recorrer dicha estructura interna una sola vez y generar una representación en JSON limpia del árbol de marcadores —título, página destino e hijos anidados— además de un conjunto resuelto de destinos de enlace, todo independientemente del comportamiento específico de la plataforma del renderizador nativo. Este paso de extracción debe ejecutarse una sola vez rath
Nota de Expo:
react-native-pdfdepende de módulos nativos, por lo que no funcionará dentro del cliente estándar de Expo Go. Se requiere una compilación personalizada o un proyecto nativo puro.
La arquitectura
El sistema está organizado en tres capas, cada una aislada del funcionamiento interno de las demás:
- Visor de PDF — registra la página actual, el número total de páginas, el estado de carga y cualquier estado de error.
- Barras de herramientas del lector — muestra detalles de la página actual y proporciona acceso al panel de marcadores.
- Panel de marcadores — gestiona el campo de búsqueda, el árbol de marcadores expandible (construido a partir de datos extraídos mediante
pdf-lib) y comunica una sola información: qué página seleccionó el usuario.
El panel de marcadores no tiene conocimiento alguno sobre cómo se renderiza el PDF en sí, y el visor de PDF tampoco sabe cómo se implementa la búsqueda de marcadores. Este flujo unidireccional —el panel selecciona una página y la pantalla indica al visor que navegue hasta allí— es lo que impide que acciones como escribir en un campo de búsqueda o expandir un nodo del árbol provoquen una costosa re-renderización del componente PDF en sí.
Paso 1: Estructurar los datos de marcadores
Cada marcador con el que trabajas necesita tres elementos: un título, una página de destino y, opcionalmente, un array de marcadores hijos; se trata de una estructura recursiva sencilla que coincide naturalmente con lo que devuelve el paso de extracción de pdf-lib. Lo más importante aquí es que cada marcador debe contar con un identificador único y estable, y ese identificador no puede ser simplemente su posición en el array. Una vez que el usuario comienza a escribir en la búsqueda o a activar y desactivar ramas, la posición de un marcador determinado en la lista mostrada cambiará. Si te basas en el índice del array como identificador, React pierde la capacidad de distinguir una fila de otra, el estado de expansión/contracción se asigna al nodo incorrecto y FlatList comienza a reciclar filas de forma errónea.
Paso 2: Buscar sin perder el contexto
Dado que un marcador coincidente suele encontrarse anidado en varios niveles dentro del árbol, su lógica de búsqueda necesita recorrer la estructura de forma recursiva en lugar de solo escanear los títulos de nivel superior. Igualmente importante: cada vez que un nodo hijo coincida con el término de búsqueda, también debe incluirse su nodo padre en los resultados, incluso si el título del padre en sí no coincide con nada. Si se omite esto, un resultado profundamente anidado aparecerá flotando sin indicación alguna de dónde se encuentra realmente en el documento.
Para mantener esta eficiencia, envuelva la lógica de filtrado en un mecanismo de memorización para que solo se ejecute nuevamente cuando cambie la cadena de búsqueda o los datos del marcador subyacente. En árboles con un gran número de entradas, agregue también un sistema de amortiguación en la entrada de texto, de modo que no se vuelva a filtrar toda la estructura con cada pulsación de tecla.
Paso 3: Plana solo los nodos visibles
FlatList es la herramienta adecuada aquí por razones de rendimiento, pero espera un array plano, no una estructura de árbol anidada. Por lo tanto, se necesita un proceso que recorra el árbol de marcadores y genere un array plano que contenga únicamente los nodos que deben estar visibles en ese momento; los hijos de un nodo solo se incluyen si dicho nodo está actualmente expandido. Cada entrada plana también debe llevar un número depth para que se sepa hasta qué punto indentarla visualmente.
Este paso es, sin duda, la decisión más importante en términos de rendimiento en toda la implementación. Si se aplanara el árbol completo y solo se ocultaran las filas pertenecientes a ramas plegadas, FlatList seguiría conservando miles de filas que nunca aparecen en la pantalla. Al aplanar únicamente lo que está actualmente expandido, un árbol de marcadores con miles de entradas puede terminar entregando a FlatList solo unas pocas docenas de filas en cualquier momento dado.
Paso 4: Construir el panel de marcadores
Implemente el panel como un modal superpuesto sobre el visor de PDF en lugar de una pantalla que lo reemplace, y limite su altura a aproximadamente el 80 % de la pantalla en lugar de permitir que ocupe toda la pantalla. De esta manera queda visible una pequeña parte del PDF debajo de él para poder orientarse, y se dispone de espacio para un botón de cierre evidente, en lugar de que el panel parezca una pantalla completamente separada.
Dentro del modal se deben incluir: un campo de búsqueda conectado a la lógica del Paso 2; un FlatList que muestre las filas procesadas en el Paso 3, configurado con propiedades de visualización como initialNumToRender, maxToRenderPerBatch, windowSize y removeClippedSubviews, de modo que solo las filas cercanas al área visible se muestren realmente; y un estado vacío como alternativa cuando la búsqueda no devuelva resultados. Cada fila debe tener dos puntos de interacción distintos: al tocar el cuerpo de la fila se selecciona esa página y se cierra el panel, mientras que al tocar un ícono pequeño de expansión se activan o desactivan sus elementos hijos, evitando así que la apertura de una rama active accidentalmente la navegación. Envuelva cada componente de fila en memo para que la expansión de una rama no obligue a volver a renderizar el resto de la lista. Además, siempre que el campo de búsqueda no esté vacío, expanda automáticamente cualquier rama correspondiente.
Paso 5: Crear la pantalla del lector de PDF
En esta pantalla tiene lugar el procesamiento real, a través del componente Pdf. Utilice la función de callback al cambiar de página para hacer un seguimiento de la página actual y del número total de páginas, muestre el indicador de carga integrado de la biblioteca mientras se carga el documento, y conecte la función de callback de errores para que, en caso de fallo, la pantalla muestre un mensaje claro como “no se pudo abrir este documento” con una opción para intentarlo de nuevo; no permita que el spinner de carga siga girando indefinidamente sin una solución alternativa.
Cada vez que navegue a una página desde un marcador, limite el número de página destino para que esté entre 1 y el conteo total conocido de páginas. Esto evita que un destino dañado u obsoleto — ya sea proveniente de los datos del marcador mismo o de un enlace interno resuelto — intente enviar al usuario a un número de página que simplemente no existe. También es recomendable activar el caché para el archivo PDF de origen, de modo que un documento ya descargado no se vuelva a descargar cada vez que se muestre esta pantalla.
En cuanto al estilo visual, esto dependerá naturalmente del sistema de diseño propio de su aplicación; sin embargo, desde el punto de vista estructural, lo importante para la usabilidad es mantener el panel con una altura cercana al 80 %, ofrecer una forma evidente de cerrarlo, proporcionar a cada fila (en particular a su ícono de expansión) un área táctil adecuada y diseñar un estado de error tranquilo y claramente distinguible.
Paso 6: Extraer una sola vez, no en cada renderizado
El paso de extracción con pdf-lib que se explicó anteriormente debe ejecutarse exactamente una vez, fuera del ciclo de renderizado, y no dentro de un componente que se vuelve a renderizar cada vez que el usuario realiza una búsqueda o expande una rama. En su lugar, almacene en caché el JSON resultante del marcador de página en lugar de volver a calcularlo repetidamente; mantenga solo los campos ligeros (IDs, títulos, números de página) en el estado de React, evite almacenar nunca datos brutos en formato PDF en el estado, y elimine cualquier recurso temporal una vez que la pantalla deje de estar activa. Si su backend puede ejecutar este paso de extracción en su lugar, ese suele ser el enfoque más adecuado: la aplicación móvil solo necesitará recibir un pequeño paquete JSON.
Paso 7: Conservar la posición del usuario
Cerrar el panel de marcadores o rotar el dispositivo no debería restablecer el lector a la primera página. Como mínimo, es necesario conservar la página actual, la rama del marcador que se expandió más recientemente y el desplazamiento de desplazamiento del panel. Almacene estos valores a nivel de la pantalla padre o del estado de navegación, en lugar de dentro del componente del panel de marcadores mismo; de esa manera seguirán existiendo incluso si el panel se desmonta.
Paso 8: Manejar los errores con delicadeza
Planifique para los fallos en el mundo real, no solo para el escenario ideal: URLs de archivos que faltan o han expirado, documentos dañados o protegidos con contraseña, dispositivos con poco espacio de almacenamiento, conexiones de red que se interrumpen durante la descarga, marcadores que apuntan a páginas inválidas o enlaces que simplemente no se abren. Sea lo que sea lo que ocurra, evite dejar al usuario mirando un indicador de carga que nunca se resuelve. Si un documento no tiene marcadores, indíquelo directamente; algo como “Este documento no contiene marcadores”. Si una búsqueda no devuelve resultados, muestre “No se encontraron marcadores”. Un estado vacío o de error bien definido es mucho mejor que una pantalla que simplemente parece estar atascada.
Paso 9: Pruebe los escenarios reales de fallo
No valide su implementación con un único PDF conveniente. Amplíe sus pruebas a estas dimensiones:
- Documentos: archivos sin marcadores en absoluto, archivos con cientos de páginas, jerarquías de marcadores profundamente anidadas, imágenes incrustadas grandes, protección con contraseña, corrupción intencionada, enlaces internos, enlaces externos y títulos que contienen caracteres especiales
- Navegación: ir a marcadores de nivel raíz o profundamente anidados, a la primera y última página, a un número de página fuera del rango, volver a abrir el panel después de cerrarlo, buscar una entrada anidada y realizar una búsqueda que no arroja resultados
- Dispositivos: un teléfono Android con poca memoria, un dispositivo de gama alta reciente, tanto iPhone como iPad, orientación vertical y horizontal, y transiciones entre el fondo y el primer plano
En este caso, probar en hardware físico es más importante que en la mayoría de las tareas relacionadas con funcionalidades, ya que un emulador simplemente no podrá reproducir la presión en la memoria y las limitaciones de almacenamiento que se experimentan en un dispositivo económico real.
Preguntas frecuentes
¿Por qué no renderizar simplemente cada página como una imagen? Un motor de renderizado nativo solo dibuja el contenido que se necesita actualmente en la pantalla, en lugar de obligar a React a gestionar un componente de imagen separado para cada una de las miles de páginas.
¿Por qué usar pdf-lib en lugar de los datos de marcadores que ya proporciona react-native-pdf? Porque esos datos integrados no están procesados, se comportan de manera inconsistente en diferentes plataformas (Windows no devuelve ninguno), y no ofrecen una forma fiable de resolver los destinos de los enlaces internos. pdf-lib analiza directamente la estructura de esquema y las anotaciones de enlaces del PDF, en una sola pasada, proporcionándote datos limpios y predecibles que puedes controlar completamente.
¿Es posible realizar búsquedas de texto completo dentro del propio documento? No con un conjunto de herramientas puramente de código abierto en este momento; esa funcionalidad está reservada actualmente para SDK comerciales. La técnica descrita aquí abarca búsquedas por títulos de marcadores, que es un problema más limitado que puedes resolver por completo por tu cuenta.
¿Debería expandirse automáticamente cada rama del árbol de marcadores? No: solo se debe expandir la rama que el usuario abre activamente. En dispositivos con memoria limitada, vale la pena restringir la interfaz de usuario para que solo una rama a nivel raíz pueda permanecer expandida en cualquier momento.
¿Algo de esto garantiza que la aplicación nunca se caerá? Ningún enfoque puede hacer esa promesa para todas las combinaciones posibles de documentos y dispositivos. El objetivo es reducir el riesgo: evitar la renderización prematura, mantener el estado almacenado al mínimo, extraer los datos solo una vez, virtualizar la lista de marcadores y manejar explícitamente cada posible fallo.
Consideraciones finales
Lograr un buen rendimiento no consiste en elegir una sola biblioteca: react-native-pdf se encarga de la renderización, pdf-lib proporciona información fiable sobre marcadores y enlaces, y la verdadera estabilidad proviene de cómo gestionas el estado, la virtualización y el manejo de errores en torno a ambas.
Las reglas prácticas a seguir:
- Ejecuta la extracción de marcadores y enlaces una sola vez mediante
pdf-lib, nunca como parte del proceso de renderización. - Nunca renders todo el árbol de marcadores a la vez; simplifica y muestra solo las secciones expandidas.
- Utiliza un
FlatListvirtualizado para almacenar la lista de marcadores. - Mantén los datos brutos en formato PDF fuera del estado de React por completo.
- Valida la destinación de una página antes de navegar a ella.
- Almacena únicamente el estado mínimo necesario para restaurar la posición del lector.