Saltar al contenido

Hermes: un motor JavaScript de código abierto optimizado para aplicaciones móviles, comenzando con React Native

Las aplicaciones móviles son cada vez más grandes y complejas. Las aplicaciones más grandes que utilizan marcos de JavaScript a menudo experimentan problemas de rendimiento a medida que los desarrolladores agregan características y complejidad. Estos problemas se generan desde varios puntos, pero las personas que usan estas aplicaciones esperan que funcionen sin problemas, independientemente del dispositivo en el que se encuentren.

Para aumentar el rendimiento de las aplicaciones de Facebook, contamos con equipos que mejoran continuamente nuestro código y plataformas JavaScript. A medida que analizamos los datos de rendimiento, nos dimos cuenta de que el motor de JavaScript en sí era un factor importante en el rendimiento de inicio y el tamaño de descarga. Con estos datos en la mano, sabíamos que teníamos que optimizar el rendimiento de JavaScript en los entornos más restringidos de un teléfono móvil en comparación con una computadora de escritorio o una computadora portátil. Después de explorar otras opciones, creamos un nuevo motor de JavaScript que llamamos Hermes. Está diseñado para mejorar el rendimiento de la aplicación, centrándose en nuestras aplicaciones React Native, incluso en dispositivos de mercado masivo con memoria limitada, almacenamiento lento y potencia informática reducida.

A , anunciamos el motor JavaScript de Hermes. Tenemos , así como la integración con Hermes para React Native. Estamos entusiasmados de trabajar con la comunidad de código abierto y que los desarrolladores comiencen a usar Hermes hoy.

Cómo Hermes mejora el rendimiento de React Native

Para las aplicaciones móviles basadas en JavaScript, la experiencia del usuario se beneficia de prestar atención a algunas métricas principales:

  • El tiempo que tarda la aplicación en ser utilizable, denominado tiempo de interacción (TTI)
  • El tamaño de descarga (en Android, tamaño APK)
  • Utilización de la memoria
Métricas para la aplicación MatterMost React Native que se ejecuta en un Google Pixel, similar en rendimiento a los teléfonos populares en mercados como India.
Métricas para la aplicación MatterMost React Native que se ejecuta en un Google Pixel, similar en rendimiento a los teléfonos populares en mercados como India.

En particular, nuestras métricas principales son relativamente insensibles al uso de la CPU del motor al ejecutar código JavaScript. Centrarse en estas métricas conduce a estrategias y compensaciones que difieren de la mayoría de los motores JavaScript existentes en la actualidad. En consecuencia, nuestro equipo diseñó y construyó Hermes desde cero. Como resultado de este enfoque, nuestra implementación proporciona una mejora sustancial para las aplicaciones React Native.

Debido a que Hermes está optimizado para aplicaciones móviles, no tenemos planes de integrarlo con ningún navegador o con infraestructura de servidor como Node.js. Los motores JavaScript existentes siguen siendo preferibles en esos entornos.

Decisiones arquitectónicas clave de Hermes

Las limitaciones de los dispositivos móviles, como cantidades más pequeñas de RAM y flash más lento, nos llevaron a tomar ciertas decisiones arquitectónicas. Para optimizar para este entorno, implementamos lo siguiente:

Precompilación de códigos de bytes

Por lo general, un motor de JavaScript analizará la fuente de JavaScript después de que se cargue, generando un código de bytes. Este paso retrasa el inicio de la ejecución de JavaScript. Para omitir este paso, Hermes usa un compilador anticipado, que se ejecuta como parte del proceso de compilación de la aplicación móvil. Como resultado, se puede dedicar más tiempo a optimizar el código de bytes, por lo que el código de bytes es más pequeño y más eficiente. Se pueden realizar optimizaciones de todo el programa, como la deduplicación de funciones y el empaquetado de tablas de cadenas.

El código de bytes está diseñado para que, en tiempo de ejecución, se pueda mapear en la memoria e interpretar sin necesidad de leer con entusiasmo todo el archivo. La E / S de memoria flash agrega una latencia significativa en muchos dispositivos móviles de gama media y baja, por lo que cargar el código de bytes desde la memoria flash solo cuando es necesario y optimizar el código de bytes para el tamaño conduce a mejoras significativas de TTI. Además, debido a que la memoria está asignada como de solo lectura y está respaldada por un archivo, los sistemas operativos móviles que no se intercambian, como Android, aún pueden desalojar estas páginas bajo presión de memoria. Esto reduce las muertes de procesos por falta de memoria en dispositivos con limitaciones de memoria.

Aunque el código de bytes comprimido es un poco más grande que el código fuente de JavaScript comprimido, debido a que el tamaño del código nativo de Hermes es más pequeño, Hermes reduce el tamaño general de la aplicación para las aplicaciones Android React Native.

Sin JIT

Para acelerar la ejecución, los motores de JavaScript más utilizados pueden compilar de forma perezosa código interpretado con frecuencia en código de máquina. Este trabajo lo realiza un compilador Just-In-Time (JIT).

Hermes hoy no tiene compilador JIT. Esto significa que Hermes tiene un rendimiento inferior a algunos puntos de referencia, especialmente aquellos que dependen del rendimiento de la CPU. Esta fue una elección intencional: estos puntos de referencia generalmente no son representativos de las cargas de trabajo de aplicaciones móviles. Hemos hecho algunos experimentos con JIT, pero creemos que sería bastante difícil lograr mejoras de velocidad beneficiosas sin retroceder nuestras métricas principales. Debido a que los JIT deben calentarse cuando se inicia una aplicación, tienen problemas para mejorar el TTI e incluso pueden dañar el TTI. Además, un JIT se suma al tamaño del código nativo y al consumo de memoria, lo que afecta negativamente nuestras métricas primarias. Es probable que un JIT perjudique las métricas que más nos interesan, por lo que decidimos no implementar un JIT. En cambio, nos enfocamos en el desempeño del intérprete como la compensación correcta para Hermes.

Estrategia del recolector de basura

En los dispositivos móviles, el uso eficiente de la memoria es especialmente importante. Los dispositivos de gama baja tienen memoria limitada, el intercambio de sistemas operativos generalmente no existe y los sistemas operativos eliminan agresivamente las aplicaciones que usan demasiada memoria. Cuando se eliminan las aplicaciones, se requieren reinicios lentos y la funcionalidad en segundo plano se ve afectada. En las primeras pruebas, aprendimos que el espacio de direcciones virtuales (VA), especialmente el espacio VA contiguo, puede ser un recurso limitado en aplicaciones grandes en dispositivos de 32 bits, incluso con una asignación diferida de páginas físicas.

Para minimizar la memoria y el espacio VA utilizado por el motor, hemos construido un recolector de basura con las siguientes características:

  • Asignación bajo demanda: asigna espacio de VA en fragmentos solo según sea necesario.
  • No contiguo: el espacio VA no necesita estar en un solo rango de memoria, lo que evita los límites de recursos en dispositivos de 32 bits.
  • Mover: poder mover objetos significa que la memoria se puede desfragmentar y los fragmentos que ya no se necesitan se devuelven al sistema operativo.
  • Generacional: no escanear todo el montón de JavaScript en cada GC reduce los tiempos de GC.

Experiencia de desarrollador

Para comenzar a usar Hermes, los desarrolladores deberán realizar algunos cambios en su build.gradle archivos y vuelva a compilar la aplicación. Vea las instrucciones completas para la migración para usar Hermes en React Native.

 project.ext.react = [
  entryFile: "index.js",
  enableHermes: true
]

Compilación perezosa

La velocidad de iteración es uno de los principales beneficios de una plataforma basada en JavaScript, pero compilar el código de bytes por adelantado reduciría esta ventaja. Para mantener las recargas rápidas, las compilaciones de depuración de Hermes no utilizan la compilación anticipada; en su lugar, generan código de bytes de forma perezosa en el dispositivo. Esto permite una iteración rápida utilizando Metro u otra fuente de código JavaScript simple para ejecutar. La compensación es que el código de bytes compilado de forma diferida no incluye todas las optimizaciones de una compilación de producción. En la práctica, aunque podemos medir la diferencia en el rendimiento, hemos descubierto que este enfoque es suficiente para proporcionar una buena experiencia de desarrollador sin afectar las métricas de producción.

Cumple con los estándares

Hermes actualmente apunta a la especificación ES6, y tenemos la intención de mantenernos actualizados con la especificación de JavaScript a medida que evoluciona. Para mantener el tamaño del motor pequeño, hemos optado por no admitir algunas funciones de idioma que no parecen ser de uso común en aplicaciones React Native, como proxies y locales. eval(). Puede encontrar una lista completa en nuestro GitHub.

Depuración

Para brindar una excelente experiencia de depuración, implementamos soporte para la depuración remota de Chrome a través del protocolo DevTools. Hasta hoy, React Native ha admitido la depuración utilizando solo un proxy en la aplicación para ejecutar el código JavaScript de la aplicación en Chrome. Este soporte hizo posible depurar aplicaciones, pero no llamadas nativas sincrónicas en el puente React Native. La compatibilidad con el protocolo de depuración remota permite a los desarrolladores conectarse al motor Hermes que se ejecuta en su dispositivo y depurar sus aplicaciones de forma nativa, utilizando el mismo motor que en producción. También estamos buscando implementar soporte adicional para el protocolo Chrome DevTools además de la depuración.

Habilitando mejoras para React Native

Para facilitar los esfuerzos de migración a Hermes y seguir admitiendo JavaScriptCore en iOS, creamos JSI, una API ligera para incrustar un motor JavaScript en una aplicación C ++. Esta API ha hecho posible que los ingenieros de React Native implementen sus propias mejoras de infraestructura. JSI es utilizado por Fabric, que permite la apropiación de la representación React Native, y por TurboModules, que permiten módulos nativos más livianos que pueden cargarse de forma diferida según sea necesario mediante una aplicación React Native.

React Native fue nuestro caso de uso inicial y ha informado gran parte de nuestro trabajo hasta la fecha, pero no nos detendremos allí. Tenemos la intención de crear herramientas de creación de perfiles de tiempo y memoria para facilitar a los desarrolladores la mejora de sus aplicaciones. Nos gustaría ser totalmente compatibles con el protocolo depurador de código de Visual Studio, incluida la finalización y otras funciones que no están disponibles en la actualidad. También nos gustaría ver otros casos de uso de dispositivos móviles.

Ningún proyecto de código abierto puede tener éxito sin el compromiso de la comunidad. Nos encantaría que lo hicieras , mira cómo funciona y ayúdanos . Estamos especialmente interesados ​​en ver qué casos de uso encuentra útiles la comunidad, tanto dentro como fuera de React Native.

Nos gustaría agradecer a Tzvetan Mikov, Will Holen y al resto del equipo de Hermes por su trabajo para construir Hermes de código abierto.

La publicación Hermes: un motor de JavaScript de código abierto optimizado para aplicaciones móviles, comenzando con React Native apareció primero en Facebook Code.