Saltar al contenido principal

Hacia que Hermes sea el valor predeterminado

· 14 min de lectura
Xuan Huang
Xuan Huang
Software Engineer @ Meta
Traducción Beta No Oficial

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:

  1. 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%.

  2. 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 introduciendo queueMicrotask, 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:

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.