De Bridge a JSI: Cómo decidir cuándo migrar a la nueva arquitectura de React Native
Cómo JSI, Fabric y TurboModules reemplazan el antiguo puente de React Native, qué significan en la práctica las mejoras reportadas, y cómo auditar y migrar una aplicación existente de forma segura.
Fuentes: React Native’s New Architecture Finally Stopped Being a Compromise
Cualquiera que haya enviado una aplicación React Native compleja con la arquitectura antigua ha observado cómo el desplazamiento rápido se interrumpe en cuanto el hilo de JavaScript se ocupa de otra tarea, como analizar una respuesta de red o propagar un cambio de estado a través de un gran árbol de componentes. Esa interrupción no era mala suerte; era una consecuencia directa de cómo funcionaba el antiguo puente de comunicación. La Nueva Arquitectura, que lo reemplaza, ha sido la predeterminada desde React Native 0.76, pero durante mucho tiempo muchos equipos la consideraron algo que adoptar más adelante. Este artículo explica qué cambian realmente las tres partes de la Nueva Arquitectura, cómo interpretar las afirmaciones sobre su rendimiento y cómo decidir si y cómo migrar una aplicación existente.
Por qué el antiguo puente causaba interrupciones
En la arquitectura heredada, toda interacción entre JavaScript y código nativo pasaba por un puente asíncrono. Una llamada se serializaba a JSON, se ponía en cola en el puente, se deserializaba en el lado nativo y se ejecutaba allí, con los resultados que regresaban por el mismo camino. De este diseño se derivan dos consecuencias: cada paso requería un costo de serialización, y como el puente era asíncrono y ordenado, todo lo destinado al código nativo tenía que esperar cada vez que el hilo de JavaScript estaba ocupado. Por lo tanto, un hilo de JavaScript ocupado bloqueaba el desplazamiento, el diseño de la interfaz y los gestos que dependían de él.
Esa alternativa ha existido de alguna forma durante años y oficialmente se ha convertido en la predeterminada desde la versión 0.76. Sin embargo, ser la opción por defecto y ser algo en lo que un equipo cauteloso confiaría para una aplicación en producción son cosas distintas. Las versiones lanzadas en la primera mitad de este año, React Native 0.85 seguido por Expo SDK 56, son consideradas ampliamente como el momento en que esa brecha se cerró; los detalles se explican más adelante.
Las tres partes de la Nueva Arquitectura
Dejando de lado el marketing, la Nueva Arquitectura consta de tres componentes que trabajan juntos, cada uno reemplazando una parte específica del sistema de conexión.
JSI: llamadas directas en lugar de mensajes
La Interfaz de JavaScript (JSI) es la base. Se trata de una interfaz ligera en C++ a través de la cual JavaScript puede mantener referencias a objetos nativos y llamar directamente a funciones nativas, incluso de forma síncrona cuando eso es apropiado. No existe serialización en JSON, ni cola de puente, ni viajes asincrónicos para las llamadas que no lo requieren. El antiguo proceso de serialización, colocación en cola, deserialización y envío se reduce esencialmente a una llamada a función en C++.
Fabric: un árbol sombra compartido en C++
Fabric es el sistema de renderizado basado en JSI. Organiza el proceso de renderizado en tres capas: el árbol de fibra de React en JavaScript, un árbol de sombra y el árbol de vistas nativas del host. La diferencia clave es que ahora el árbol de sombra se encuentra en código C++ compartido, en lugar de ser gestionado a través del puente desde JavaScript. El diseño con Yoga se calcula en esa capa compartida, por lo que los datos de layout ya no deben pasar a través del puente.
Fabric también es lo que permite que las funcionalidades concurrentes de React, como las transiciones, Suspense y useTransition, funcionen en React Native. Su fase de confirmación puede ejecutarse de forma síncrona o concurrente, y el renderizador basado en puente no tenía la capacidad de soportar adecuadamente ninguno de los dos modos. Si su equipo desea el mismo modelo de renderizado concurrente que utiliza en la web, Fabric es un requisito previo y no una optimización.
TurboModules: módulos nativos perezosos y tipados
TurboModules reemplazan el sistema heredado NativeModules para acceder a la funcionalidad nativa. El fragmento muestra las diferencias entre ambos enfoques. La importación antigua obtiene un módulo de NativeModules, que se inicializaban de inmediato al arrancar y generaban costos de serialización en cada llamada. La importación nueva carga un módulo generado a partir de una especificación tipada, respaldado por JSI, que se inicializa solo la primera vez que se utiliza y puede llamarse de forma síncrona cuando sea apropiado.
// Old NativeModules: loaded eagerly at startup, whether used or not,
// with every call paying the bridge's serialization cost
import { NativeModules } from 'react-native';
const { CameraModule } = NativeModules;
// TurboModules: JSI-backed, lazily loaded only when actually called,
// with type-safe interfaces generated by Codegen from your spec
import CameraModule from './specs/NativeCameraModule';
CameraModule.capturePhoto(); // direct, synchronous-capable call via JSI
El cargado diferido es más importante de lo que parece a primera vista. En el sistema heredado, una aplicación con muchos módulos nativos pagaba un costo de inicialización al arrancar por cada uno, incluso si una sesión determinada nunca utilizaba la mayoría de ellos. TurboModules solo paga cuando un módulo se utiliza realmente. La especificación tipada representa la otra parte de esta ventaja: Codegen la convierte en interfaces nativas, lo cual se explica en detalle en nuestro artículo sobre por qué Codegen hace obligatoria la especificación primero para TurboModules.
Leer con atención las afirmaciones sobre el rendimiento
Las cifras citadas para la Nueva Arquitectura varían según la fuente y la carga de trabajo, por lo que deben considerarse como una orientación y no como valores concretos para prometer a las partes interesadas. Los resultados reportados incluyen:
- un rendimiento del hilo de interfaz aproximadamente un 10 a un 30% mejor en las aplicaciones típicas, llegando hasta 3 veces mayor en aquellas que dependen en gran medida de módulos nativos
- llamadas desde JavaScript hacia el código nativo un 20 a un 50% más rápidas
- tiempos de inicio un 20 a un 40% más cortos, dependiendo de cuántos módulos nativos tenga una aplicación y de qué operaciones se midan
Un indicador más útil que cualquier porcentaje individual es saber qué categorías de aplicaciones pasaron de ser solucionables con workarounds a ser sencillas de implementar. Las aplicaciones que utilizan mucho la cámara, la visualización en tiempo real de datos, las interfaces animadas complejas y todo aquello que requiere mediciones sincrónicas reales del diseño necesitaban trucos arquitectónicos sofisticados o simplemente no eran prácticas. Con la Nueva Arquitectura, se convierten en tareas de ingeniería ordinarias. Si su aplicación pertenece a una de esas categorías, la motivación para migrar es fuerte independientemente de las cifras promedio; si se trata principalmente de formularios y listas, los beneficios serán reales pero más modestos. Las únicas cifras que deben influir en su decisión son las que usted mismo mida en su aplicación antes y después.
Lo que arregló el reciente ciclo de lanzamientos
La razón por la cual este año se considera un punto de inflexión no es una nueva funcionalidad, sino un conjunto de correcciones para los problemas específicos que hacían esperar a los equipos cautelosos. Consulte las notas oficiales de lanzamiento para obtener detalles exactos, ya que el resumen a continuación refleja los informes al momento de redactar este texto.
- React Native 0.85, lanzado en abril, reconstruyó el sistema de animaciones compartido y eliminó una causa persistente de pérdida de fotogramas. Se informa que las transiciones con muchas animaciones mantienen 60 fps en la mayoría de los dispositivos compatibles, en lugar de deteriorarse bajo carga.
Migración en la práctica: primero la auditoría de dependencias
Nada de esto implica que todas las aplicaciones existentes deban cambiar mañana mismo. Lo primero a verificar antes de realizar cambios es la compatibilidad con las bibliotecas. En el momento de escribir esto, se informa que alrededor del 85% de los paquetes populares de React Native en npm son compatibles con la Nueva Arquitectura, lo que deja aproximadamente un 15% que solo son compatibles mediante puentes o de forma parcial. Las migraciones estancadas casi siempre se quedan en ese 15%, no en el propio framework, sino en algunos módulos nativos de terceros de los que depende la aplicación.
Un patrón que se repite entre los equipos que migran con éxito es el siguiente:
- Haga una auditoría antes de comenzar. Enumere todas las dependencias nativas y verifique si son compatibles con la nueva arquitectura según su versión actual, no basándose en lo que recuerde de hace uno o dos años. Para cada paquete no compatible, decida si actualizarlo, reemplazarlo, crear una copia modificada o manejarlo usted mismo.
- Aproveche Codegen. Dado que Codegen genera interfaces nativas seguras desde sus especificaciones en TypeScript o JavaScript, toda clase de incompatibilidades entre JavaScript y código nativo se detectan en el momento de la compilación, en lugar de causar fallos en tiempo de ejecución. Si escribe sus propios módulos, nuestra guía sobre conectar un módulo Turbo dentro de la aplicación de extremo a extremo con Codegen muestra todo el proceso.
Orientación según la situación
Iniciar un proyecto nuevo
Hay pocos motivos para evitarlo. La Nueva Arquitectura es la predeterminada; las herramientas asociadas, en particular el Expo SDK 56, asumen que se utiliza esta arquitectura, y la brecha de compatibilidad es ahora lo suficientemente pequeña como para que sea poco probable encontrar obstáculos, a menos que la aplicación tenga dependencias nativas inusuales.
Mantener una aplicación en el puente heredado
Comience con la auditoría de compatibilidad antes de tocar cualquier otra cosa, utilizando su lista real de dependencias. La proporción de paquetes compatibles ha ido en aumento constante, por lo que una evaluación realizada hace un año probablemente esté desactualizada. Planifique una migración gradual y considere los errores que presenta Codegen como una verdadera red de seguridad durante la transición, y no solo como un trámite formal.
Equipos que lo evaluaron antes y decidieron esperar
Posponerlo a 2023 o 2024 fue una decisión razonable dadas las deficiencias de la época. La reconstrucción de la animación y las correcciones de fugas de memoria en la versión 0.85 abordan precisamente las preocupaciones relacionadas con la estabilidad en producción que justificaron la espera, lo que convierte a este ciclo de lanzamientos en el momento adecuado para volver a plantear la cuestión.
Puntos clave
- El puente heredado serializaba cada llamada y la ponía en cola detrás de un hilo de JavaScript ocupado; eso, y no la mala suerte, fue la causa del mal rendimiento histórico.
- JSI permite llamadas directas, Fabric traslada el árbol de sombras y el diseño a C++ compartido y habilita React concurrente, mientras que TurboModules se cargan de forma perezosa a partir de especificaciones tipadas.
- Las mejoras en velocidad varían mucho; tome decisiones según la categoría de su aplicación y sus propias mediciones.
- Lo que ha cambiado recientemente es la estabilidad y las herramientas, no las funcionalidades: la fiabilidad de las animaciones, el comportamiento de la memoria en sesiones largas y la velocidad de compilación.
- El riesgo de migración radica en los módulos nativos de terceros, así que audite primero las dependencias, confíe en Codegen y mueva una pantalla o módulo a la vez.
Lecturas relacionadas
- Por qué Codegen hace obligatorio el enfoque en la especificación primero para React Native TurboModules — Vea cómo Codegen de React Native convierte la especificación en TypeScript de TurboModule en un contrato en tiempo de compilación, qué falla al omitirla y dónde deja de aplicarse la regla.
- Nitro Modules: Cómo las vinculaciones estáticas superan a React Native TurboModules — Explica cómo Nitro Modules utiliza vinculaciones estáticas precompiladas en lugar de búsquedas dinámicas para superar significativamente a TurboModules y Expo Modules en React Native.