- Porffor es un proyecto de investigación que compila JavaScript por adelantado, en lugar de en tiempo de ejecución, para convertirlo en WebAssembly y binarios nativos
- Gracias a que no empaqueta un intérprete, apunta a que la salida sea 10 a 30 veces más pequeña y rápida que en proyectos existentes de JS→Wasm
- Incluso en compilaciones nativas, no empaqueta un runtime, por lo que el tamaño del binario puede reducirse hasta 1000 veces; el ejemplo mostrado baja de unos 90MB a menos de 100KB
- Está hecho en JS, no tiene
evaly propone una arquitectura con soporte nativo para TypeScript sin una etapa de build separada - AOT favorece la optimización mediante análisis estático y la compilación previa a la ejecución, pero dificulta la evaluación dinámica de JS como
eval, y todavía está en una fase temprana donde mucho código JS aún no funciona
Cómo ejecuta el código Porffor
- Porffor es un proyecto de investigación que compila JavaScript Ahead-of-Time a WebAssembly y binarios nativos
- Un binario de TypeScript compilado con Porffor sirve esta página
- Fue escrito desde el inicio pensando en AOT, con una estructura que intenta optimizaciones que eran difíciles en las formas tradicionales de ejecutar JS
Diferencias entre la salida WebAssembly y la nativa
-
JS → Wasm
- La salida WebAssembly de Porffor es 10 a 30 veces más pequeña y rápida que la de proyectos existentes de JS→Wasm
- La diferencia clave está en que compila JS directamente y no empaqueta un intérprete
- Ejecutar JS sobre Wasm permite una ejecución aislada en sandbox, pero puede implicar una gran pérdida de rendimiento, y Porffor se enfoca en reducir ese costo
- Casos de uso posibles:
- Hosting de JS del lado del servidor: en runtimes de edge, el sandboxing con Wasm puede ofrecer ejecución segura sin necesidad de aislamiento excesivo
- El bajo overhead del AOT, frente a JIT, abre la posibilidad de ejecutar más clientes sobre el mismo hardware con una pérdida mínima de rendimiento
- Resistencia a la ingeniería inversa: para JS sensible, el código compilado puede ser más difícil de analizar que el código ofuscado
-
JS → Native
- Como realmente compila JS y no empaqueta un runtime, el tamaño del binario puede ser hasta 1000 veces menor
- El tamaño de ejemplo es de aproximadamente 90MB → menos de 100KB
- Internamente compila JS a C y luego lo compila a nativo, así que donde se pueda usar C, se puede usar JS
- Casos de uso posibles:
- Ejecución de JS rápido en sistemas embebidos, consolas de videojuegos, etc.
- Pequeñas apps CLI en JS compiladas como ejecutables de un solo clic de menos de 1MB
Ventajas y limitaciones del AOT
- Los intérpretes tradicionales o las múltiples etapas de JIT tienen que equilibrar el tiempo de arranque y el rendimiento de JS
- AOT compila primero y ejecuta después, así que la velocidad de compilación importa para la experiencia del desarrollador, pero no afecta la experiencia del usuario
- Este enfoque deja espacio para hacer optimizaciones basadas en análisis estático como en C++ y Rust
- La principal desventaja es que no existe evaluación dinámica de JS como
eval, y que hay que crear un motor JS nuevo - Como aún está en una fase temprana, mucho código JS todavía no funciona, pero el trabajo de mejora continúa
- Para seguir el progreso de compatibilidad con ECMAScript, ejecuta la suite oficial de pruebas Test262 en cada commit
1 comentarios
Opiniones en Hacker News
Oliver, el principal desarrollador de Porffor, anunció que trabajará tiempo completo en Porffor: https://x.com/canadahonk/status/1818347311417938237
https://news.ycombinator.com/user?id=defunkt
He pensado en algo parecido, pero creo que es difícil lograr un rendimiento mucho mejor en JavaScript. Probablemente lo mejor sea algo como transpilar JS a llamadas C++ de V8
Las optimizaciones realmente interesantes aparecen al compilar TypeScript o algo cercano. Si se aprovechan los tipos, se pueden obtener grandes beneficios, y las partes sin tipos básicamente caerían en llamadas JS lentas. Las interfaces podrían reducirse a tablas de funciones virtuales o llamadas directas, y también se podría operar sobre structs en vez de mapas. Se podrían tener tipos
IntyFloat, degradarlos aNumbercuando sea necesario y mantenerlos en registrosEl problema central es que tanto TS como V8 son objetivos no estándar que cambian rápido. Un proyecto así requiere un equipo grande, y mantener la compatibilidad se vuelve un trabajo aparte
Como ejemplo simple, TypeScript no distingue entre enteros y punto flotante; trata todo como números. Por eso, cada acceso a un arreglo requiere conversión de tipos. Si TypeScript hubiera sido diseñado para ayudar a la compilación estática, probablemente tendría esa distinción
El problema mayor es el subtipado estructural de TypeScript. Por esta característica, para el compilador es prácticamente imposible determinar estáticamente la estructura física de los argumentos no primitivos que se pasan a una función. Un JIT puede hacer análisis dinámico de formas, así que en cada acceso a campos podría rendir peor que un JIT
Ha habido mucho trabajo en herramientas de análisis estático de tipos para JS, y también son posibles análisis muy exhaustivos. Un ejemplo que se me viene a la mente, aunque algo antiguo, es TAJS
Sería bueno que incluso TypeScript permitiera especificar tipos como
integer. Aunque en una compilación TS→JS comúnconst val: intse tratara igual queconst val: number, un runtime moderno que entienda TS podría aprovechar esa información adicionalMe pregunto si una sintaxis como
const counter: Numberpodría llegar a aceptarseNo sé si el sitio cambió o si me estoy perdiendo algo
En windmill.dev, cuando los usuarios despliegan código, usan Bun build para empaquetar el script y todas sus dependencias en un único archivo JS, que luego cargan para mejorar el cold start y el uso de memoria. Por el tamaño del bundle, el resultado se guarda en S3
Si se pudiera empaquetar todo de forma nativa, sería un cambio total. Por muy bueno que sea el cold start de Bun, le costaría competir contra ejecutar nativamente un binario pequeño de inmediato
Es bueno ver más runtimes de JS acercándose a Wasm. Este proyecto me recuerda a Static Hermes, el motor JS de Facebook para acelerar iOS y Android en proyectos de React Native.
Ambos apuntan a cumplir con JS test262, y mientras Porffor admite tanto salida nativa como Wasm, Static Hermes por ahora se enfoca principalmente en la salida nativa. Porffor está escrito en JS puro y va en la dirección de poder compilarse a sí mismo, mientras que Static Hermes depende de LLVM. Porffor todavía tenía soporte limitado para async/promise/await, y Static Hermes lo admite con algunas limitaciones. Static Hermes está escrito en C++, y Porffor principalmente en JS. Ambos admiten TypeScript, pero Static Hermes transpila el AST de TS a Flow, mientras que Porffor lo admite de forma nativa. Static Hermes tiene un intérprete alternativo para casos de JS difíciles de compilar, como
eval, y Porffor solo admite compilación anticipada.En general, me entusiasma ver si este proyecto gana tracción y logra hacer más rápidos los motores de JavaScript en el edge. Lo dejo como Syrus de Wasmer.
https://github.com/facebook/hermes/discussions/1137
https://github.com/tc39/test262
https://wasmer.io
Sin embargo, no es nuestro foco; nos concentramos principalmente en React Native. En ese entorno, WASM no tiene mucho sentido.
La función más importante de Static Hermes es su verificador de tipos, que garantiza la solidez en tiempo de ejecución. Porffor es muy interesante; lo he estado siguiendo desde hace un tiempo y le deseo lo mejor.
Es un enfoque similar al de Kiesel: https://kiesel.dev/
Array.prototype.filter,Math.sinyatobsí se compilan parcialmente a sí mismas.Recientemente, Porffor también empezó a admitir async/promise/await básico. Todavía no funciona tan bien.
JavaScript tiene un subconjunto que se puede compilar fácilmente, y lo difícil es la larga cola que queda fuera. Aun así, es genial que se esté investigando dónde está ese límite y cuánto beneficio se puede obtener de ese subconjunto.
Me encanta de verdad que admita
String.blink. Que los desarrolladores tengan humor y espíritu juguetón siempre es una buena señal.La implementación también es trivial, algo como
function() { return "" + this + ""; }, así que vale la pena implementarlo incluso si el host de ECMAScript no es un navegador web. En ese caso es opcional. No esperaría que esto tenga que ver con “humor o espíritu juguetón”.String.blinkestá en test262, así que, si el proyecto quiere cumplir su objetivo, en la práctica tiene que admitirlo.Me pregunto cuál es el matiz que se me escapa. No entiendo por qué “motor JS de compilación anticipada” es una mejor descripción que “compilador de JS a Wasm”. Si es principalmente una estrategia de encuadre, también está bien.
El esquema de versionado descrito aquí me parece un poco sospechoso.
Si algún cambio provoca regresiones en algunas pruebas de Test262, el número de versión también podría retroceder. Es decir, Porffor no puede tener al mismo tiempo números de versión que aumenten de forma monótona y la capacidad de que cambios necesarios provoquen regresiones en Test262.
https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...
En galés significa “morado”.
Es refrescante ver varios motores JS para distintos usos.
He estado trabajando en ofrecer más API compatibles con Node en quickjs mediante llrt, para embeber plugins en aplicaciones.
https://github.com/awslabs/llrt