Hacia que Hermes sea el valor predeterminado
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Desde que anunciamos Hermes en 2019, su adopción ha ido en aumento en la comunidad. El equipo de Expo, que mantiene un meta-framework popular para aplicaciones React Native, recientemente anunció soporte experimental para Hermes tras ser una de las funcionalidades más solicitadas de Expo. El equipo de Realm, una popular base de datos móvil, también lanzó recientemente su soporte alfa para Hermes. En esta publicación, queremos destacar algunos de los avances más emocionantes que hemos logrado en los últimos dos años para impulsar a Hermes como el mejor motor de JavaScript para React Native. De cara al futuro, estamos seguros de que con estas mejoras y otras por venir, podemos hacer de Hermes el motor de JavaScript predeterminado para React Native en todas las plataformas.
Optimizando para React Native
La característica definitoria de Hermes es que realiza el trabajo de compilación ahead-of-time, lo que significa que las aplicaciones React Native con Hermes habilitado se distribuyen con bytecode precompilado optimizado en lugar de código fuente JavaScript plano. Esto reduce drásticamente el trabajo necesario para iniciar tu producto para los usuarios. Mediciones tanto de Facebook como de aplicaciones comunitarias sugieren que habilitar Hermes suele reducir la métrica TTI (o Time-To-Interactive) de un producto casi a la mitad.
Dicho esto, hemos estado trabajando en mejorar Hermes en muchos otros aspectos para hacerlo aún mejor como motor de JavaScript especializado para React Native.
Construyendo un nuevo recolector de basura para Fabric
Con el próximo renderizador Fabric en la nueva arquitectura de React Native, será posible llamar a JavaScript de forma síncrona en el hilo de UI. Sin embargo, esto significa que si el hilo de JavaScript tarda demasiado en ejecutarse, puede causar caídas notables de fotogramas en la interfaz de usuario y bloquear las entradas del usuario. El renderizado concurrente habilitado por React Fiber evitará programar tareas largas de JavaScript dividiendo el trabajo de renderizado en fragmentos. Sin embargo, existe otra fuente común de latencia proveniente del hilo de JavaScript: cuando el motor de JavaScript debe "detener el mundo" para realizar una recolección de basura (GC).
El recolector de basura predeterminado anterior en Hermes, GenGC, era un recolector de basura generacional de un solo hilo. Las nuevas generaciones usan una estrategia típica de copia de semi-espacio, y las generaciones antiguas usan una estrategia de marca-compactación para ser realmente eficiente en devolver memoria agresivamente al sistema operativo. Debido a su naturaleza de un solo hilo, GenGC tiene el inconveniente de causar pausas largas de GC. En aplicaciones tan complejas como Facebook para Android, observamos una pausa promedio de 200 ms, o 1.4 s en p99. Incluso hemos visto que alcanza los 7 segundos, considerando la base de usuarios grande y diversa de Facebook para Android.
Para mitigar esto, implementamos un nuevo GC casi concurrente llamado Hades. Hades recolecta su generación joven igual que GenGC, pero gestiona su generación antigua con un recolector mark-sweep que toma una instantánea al inicio. Esto reduce significativamente los tiempos de pausa del GC al realizar la mayor parte del trabajo en un hilo en segundo plano, sin bloquear el hilo principal del motor que ejecuta código JavaScript. Nuestras estadísticas muestran que Hades solo pausa 48 ms en el percentil 99.9 en dispositivos de 64 bits (¡34 veces más rápido que GenGC!) y alrededor de 88 ms en el percentil 99.9 en dispositivos de 32 bits (donde opera como un GC incremental monohilo). Estas mejoras en tiempos de pausa pueden tener el costo de un rendimiento general menor, debido a barreras de escritura más costosas, asignaciones más lentas basadas en listas libres (frente a un asignador de puntero bump) y mayor fragmentación del montón. Consideramos que son compensaciones adecuadas, y logramos un consumo de memoria general más bajo mediante coalescencia y otras optimizaciones de memoria que mencionaremos.
Atacando los puntos críticos de rendimiento
El tiempo de inicio de las aplicaciones es crucial para el éxito de muchas apps, y seguimos ampliando los límites de React Native. Para cualquier nueva función de JavaScript que implementamos en Hermes, monitoreamos cuidadosamente su impacto en el rendimiento de producción y aseguramos que no degraden las métricas. En Facebook, estamos experimentando con un perfil de transformación Babel dedicado para Hermes en Metro para reemplazar una docena de transformaciones Babel con implementaciones nativas ESNext de Hermes. Observamos mejoras del 18-25% en TTI en muchas superficies y reducciones generales en el tamaño del bytecode, y esperamos ver resultados similares en código abierto.
Además del rendimiento en el inicio, identificamos la huella de memoria como una oportunidad de mejora en apps de React Native, especialmente para realidad virtual. Gracias al control de bajo nivel que tenemos como motor JavaScript, pudimos entregar rondas de optimizaciones de memoria exprimiendo cada bit:
-
Anteriormente, todos los valores JavaScript se representaban como valores etiquetados codificados con NaN-boxing de 64 bits para representar doubles de punto flotante y punteros en arquitecturas de 64 bits. Sin embargo, esto es ineficiente en la práctica porque la mayoría de los números son enteros pequeños (SMI) y el montón JavaScript de aplicaciones cliente generalmente no supera los 4 GiB. Para abordar esto, introdujimos una nueva codificación de 32 bits donde SMI y punteros se codifican en 29 bits (dado que los punteros están alineados a 8 bytes, podemos asumir que los últimos 3 bits son siempre cero), y el resto de números JS se encajonan en el montón. Esto redujo el tamaño del montón JavaScript en ~30%.
-
Diferentes tipos de objetos JavaScript se representan como diferentes celdas gestionadas por GC en el montón JavaScript. Al optimizar agresivamente el diseño de memoria de sus cabeceras, reducimos el uso de memoria en otro ~15%.
Una decisión clave con Hermes fue no implementar un compilador just-in-time (JIT) porque creemos que para la mayoría de apps de React Native, los costos adicionales de calentamiento y la huella extra en binarios y memoria no valdrían la pena. Durante años, invertimos esfuerzo en optimizar el rendimiento del intérprete y optimizaciones del compilador para que el rendimiento de Hermes compita con otros motores en cargas de trabajo de React Native. Seguimos enfocados en mejorar el rendimiento identificando cuellos de botella en todas partes (bucle de despacho del intérprete, diseño de pila, modelo de objetos, GC, etc.). ¡Esperen más cifras en próximas versiones!
Pioneros en integraciones verticales
En Facebook, preferimos colocar proyectos dentro de un gran monorepo. Al tener el motor (Hermes) y el host (React Native) iterando estrechamente, abrimos espacio para integraciones verticales. Por ejemplo:
-
Hermes admite depuración de JavaScript en el dispositivo con el depurador de Chrome mediante el uso del Protocolo de Chrome DevTools. Es mejor que la antigua "Depuración remota de JS" (que usa un proxy en la aplicación para ejecutar JS en Chrome de escritorio) porque admite depuración de llamadas nativas síncronas y garantiza un entorno de ejecución consistente. Junto con React DevTools, Metro, Inspector y demás, el depurador de Hermes ahora es parte de Flipper para ofrecer una experiencia de desarrollo integral.
-
Los objetos asignados durante la ruta de inicialización de apps de React Native suelen tener larga vida útil y no siguen la hipótesis generacional aprovechada por los GC generacionales. Por ello, configuramos Hermes en React Native para asignar directamente los primeros 32MiB en generaciones antiguas (conocido como pre-tenuring), evitando así pausas de GC y retrasos en el TTI.
-
La nueva arquitectura de React Native se basa fuertemente en JSI (JavaScript Interface), una API liviana de propósito general para integrar motores JavaScript en programas C++. Al tener al equipo que mantiene el motor JS también manteniendo la implementación de la API JSI, confiamos en ofrecer la mejor integración posible, probada a gran escala en Facebook, con fiabilidad y alto rendimiento.
-
Garantizar que las primitivas de concurrencia de JavaScript (ej. promesas) y las primitivas de concurrencia de la plataforma (ej. microtareas) sean tanto semánticamente correctas como eficientes es crucial para el renderizado concurrente de React y el futuro de las apps de React Native. Históricamente, las promesas en React Native se polyfillaban usando APIs no estandarizadas como
setImmediate. Estamos trabajando para hacer disponibles promesas nativas y microtareas desde motores JS mediante JSI, e introduciendoqueueMicrotask, una adición reciente al estándar web, para mejorar el soporte de código JavaScript asíncrono moderno.
Involucrando a toda la comunidad
Hermes ha sido excelente para nosotros en Facebook. Pero nuestro trabajo no estará completo hasta que la comunidad pueda usar Hermes para potenciar experiencias en todo el ecosistema, permitiendo que todos aprovechen sus características y abracen todo su potencial.
Expansión a nuevas plataformas
Hermes se lanzó inicialmente solo para React Native en Android. Desde entonces, nos emociona ver cómo miembros de la comunidad han extendido el soporte de Hermes a muchas otras plataformas que ha adoptado el ecosistema de React Native.
Callstack lideró el esfuerzo para llevar Hermes a iOS en React Native 0.64. Escribieron una serie de artículos y organizaron un podcast sobre cómo lo lograron. Según sus pruebas comparativas, Hermes logró mejorar consistentemente el arranque en ~40% y reducir la memoria en ~18% en iOS respecto a JSC para la app Mattermost, con solo 2.4 MiB de sobrecarga en el tamaño de la aplicación. Te animamos a verlo en vivo con tus propios ojos.
Microsoft ha estado incorporando Hermes a React Native para Windows y macOS. En Microsoft Build 2020, Microsoft compartió que el impacto de memoria de Hermes (conjunto de trabajo) es un 13% menor que el motor Chakra en React Native para Windows. Recientemente, en algunos benchmarks sintéticos, descubrieron que Hermes 0.8 (que incluye Hades y las optimizaciones mencionadas de SMI y compresión de punteros) utiliza entre un 30% y un 40% menos de memoria que otros motores. Como era de esperar, la experiencia de videollamadas de Messenger para escritorio, construida sobre React Native, también funciona con Hermes.
Por último, pero no menos importante, Hermes también ha estado impulsando todas las experiencias de realidad virtual construidas con la familia de tecnologías React en Oculus, incluido Oculus Home.
Apoyando a nuestra comunidad
Reconocemos que aún existen obstáculos que impiden que partes de la comunidad adopten Hermes, y estamos comprometidos a desarrollar soporte para estas características faltantes. Nuestro objetivo es ser completamente funcionales para que Hermes sea la elección correcta para la mayoría de las aplicaciones de React Native. Así es como la comunidad ya ha moldeado la hoja de ruta de Hermes:
-
ProxyyReflectoriginalmente se excluyeron de Hermes porque Facebook no los usa. También nos preocupaba que agregar Proxy perjudicaría el rendimiento de las búsquedas de propiedades incluso cuando no se usa Proxy. Pero Proxy rápidamente se convirtió en la característica más solicitada de Hermes debido a bibliotecas populares como MobX e Immer. Evaluamos cuidadosamente y decidimos implementarlo solo para la comunidad, y logramos hacerlo con un costo muy bajo. Dado que es una característica que no usamos, confiamos en nuestra comunidad para probar su estabilidad. Comenzamos probando Proxy detrás de una bandera y creamos paquetes npm opcionales para la versión v0.4 y v0.5, y está habilitado por defecto desde la v0.7. -
La Especificación de la API de Internacionalización de ECMAScript (ECMA-402, o
Intl) fue la segunda característica más solicitada.Intles un extenso conjunto de APIs que generalmente requiere incluir datos Unicode CLDR con un peso de 6MB. Por eso los polyfills como FormatJS (también conocido comoreact-intl) y motores JavaScript como la variante internacional de JSC de la comunidad tienen tamaños considerables. Para evitar aumentar significativamente el tamaño binario de Hermes, implementamos una estrategia alternativa: utilizar y mapear las capacidades ICU proporcionadas por las bibliotecas del sistema operativo, aceptando ciertas variaciones (generalmente menores) en el comportamiento entre plataformas.- Microsoft colaboró en la implementación para Android. Cubre casi todo desde ECMA-402 hasta ES2020, con un impacto de tamaño de solo el 3% (57-62K por ABI). Realizamos una encuesta en Twitter cuyos resultados apoyaron abrumadoramente incluir
Intlpor defecto, así que lo implementamos y está disponible desde la versión v0.8. - Facebook patrocinó a Major League Hacking para lanzar un programa de becas de código abierto remoto. El año pasado lanzamos el perfilador de muestreo de Hermes. Este año, nuestros becarios trabajarán con miembros de Hermes, React Native y Callstack para añadir soporte de
Intlde Hermes en iOS. ¡Estén atentos!
- Microsoft colaboró en la implementación para Android. Cubre casi todo desde ECMA-402 hasta ES2020, con un impacto de tamaño de solo el 3% (57-62K por ABI). Realizamos una encuesta en Twitter cuyos resultados apoyaron abrumadoramente incluir
-
Agradecemos a quienes han colaborado en identificar problemas que afectan a la comunidad:
- Se identificaron divergencias críticas de la especificación como la estabilidad de
Array.prototype.sortenmendada en ES2019. Esto se corrigió y estará disponible en la próxima versión. - Se descubrió que nuestro límite predeterminado del tamaño del heap era demasiado pequeño, causando presión innecesaria en el GC y crashes por falta de memoria (OOM) para usuarios que no personalizan la configuración del GC de Hermes. Por ello lo aumentamos de 512MiB a 3GiB, suficiente para la mayoría de usuarios por defecto.
- También se reportó que nuestra implementación especializada de
Function.prototype.toStringreducía el rendimiento en bibliotecas con detección inadecuada de características y bloqueaba la inyección de código fuente. Esto reforzó nuestra postura: Hermes no debe obstaculizar a los desarrolladores y debe respetar prácticas de facto.
- Se identificaron divergencias críticas de la especificación como la estabilidad de
Resumen
En resumen, nuestra visión es preparar Hermes para ser el motor JavaScript predeterminado en todas las plataformas de React Native. Ya hemos comenzado este trabajo y queremos conocer sus opiniones sobre esta dirección.
Es crucial preparar el ecosistema para una adopción fluida. Les animamos a probar Hermes y reportar problemas en nuestro repositorio de GitHub con comentarios, preguntas, solicitudes de características e incompatibilidades.
Agradecimientos
Agradecemos al equipo de Hermes, al equipo de React Native y a los numerosos colaboradores de la comunidad por su trabajo para mejorar Hermes.
Personalmente, también agradezco (en orden alfabético) a Eli White, Luna Wei, Neil Dhar, Tim Yung, Tzvetan Mikov y muchos otros por su ayuda durante la redacción.
