- Porffor est un projet de recherche qui compile JavaScript à l’avance, plutôt qu’au moment de l’exécution, pour produire du WebAssembly et des binaires natifs
- Grâce à une approche qui n’embarque pas d’interpréteur, il vise des sorties 10 à 30 fois plus petites et plus rapides que les projets JS→Wasm existants
- Pour les builds natifs également, comme il ne package pas de runtime, la taille des binaires peut diminuer jusqu’à 1000 fois ; l’exemple passe d’environ 90 Mo à moins de 100 Ko
- Il est écrit en JS, n’utilise pas
eval, et met en avant une architecture avec prise en charge native de TypeScript sans étape de build séparée - L’AOT facilite les optimisations par analyse statique et la compilation avant exécution, mais rend difficile l’évaluation dynamique de JS comme
eval, et le projet en est encore à un stade précoce où beaucoup de code JS ne fonctionne pas encore
Le mode d’exécution que crée Porffor
- Porffor est un projet de recherche qui compile JavaScript en Ahead-of-Time vers WebAssembly et des binaires natifs
- Le binaire TypeScript compilé avec Porffor sert cette page
- Conçu dès le départ pour l’AOT, il tente des optimisations difficiles à réaliser avec les modes d’exécution JS classiques
Différences entre les sorties WebAssembly et natives
-
JS → Wasm
- La sortie WebAssembly de Porffor est 10 à 30 fois plus petite et plus rapide que celles des projets JS→Wasm existants
- La différence clé est qu’il compile directement le JS, sans bundler d’interpréteur
- Exécuter du JS dans Wasm permet une exécution en sandbox, mais peut entraîner une forte perte de performances ; Porffor se concentre sur la réduction de ce coût
- Cas d’usage possibles :
- Hébergement JS côté serveur : dans les edge runtimes, le sandboxing Wasm peut offrir une exécution sûre sans isolation excessive
- Le faible overhead de l’AOT ouvre la possibilité d’exécuter davantage de clients sur le même matériel qu’avec un JIT, avec une perte de performances minimale
- Résistance au reverse engineering : pour du JS sensible, du code compilé peut être plus difficile à rétroconcevoir que du code obfusqué
-
JS → Native
- Comme le JS est réellement compilé sans packager de runtime, la taille des binaires peut être jusqu’à 1000 fois plus petite
- La taille d’exemple est d’environ 90 Mo → moins de 100 Ko
- En interne, le JS est compilé en C puis en natif ; ainsi, partout où l’on peut utiliser C, on peut utiliser JS
- Cas d’usage possibles :
- Exécution de JS rapide dans l’embarqué, les consoles de jeu, etc.
- Petites apps CLI en JS compilées en exécutables one-click de moins de 1 Mo
Avantages et limites de l’AOT
- Les interpréteurs traditionnels et les multiples niveaux de JIT doivent trouver un équilibre entre temps de démarrage et performances JS
- Avec l’AOT, on compile d’abord et on exécute ensuite ; la vitesse de compilation est donc importante pour l’expérience développeur, mais n’affecte pas l’expérience utilisateur
- Cette approche laisse de la place à des optimisations fondées sur l’analyse statique, comme en C++ et en Rust
- Les principaux inconvénients sont l’absence d’évaluation dynamique de JS comme
eval, et la nécessité de créer un nouveau moteur JS - Le projet en est encore à ses débuts, donc beaucoup de code JS ne fonctionne pas encore, mais des améliorations sont en cours
- Pour suivre l’avancement de la compatibilité ECMAScript, la suite de tests officielle Test262 est exécutée à chaque commit
1 commentaires
Commentaires sur Hacker News
Oliver, le principal développeur de Porffor, a annoncé qu’il travaillerait à plein temps sur Porffor : https://x.com/canadahonk/status/1818347311417938237
https://news.ycombinator.com/user?id=defunkt
J’ai pensé à quelque chose de similaire, mais il me semble difficile d’obtenir de bien meilleures performances en JavaScript. Au mieux, ce serait sans doute une sorte de transpilation de JS vers des appels C++ de V8
Les optimisations vraiment intéressantes apparaissent lorsqu’on compile du TypeScript ou quelque chose de proche. Exploiter les types peut apporter un gros gain, et les parties non typées retomberaient par défaut sur des appels JS lents. Les interfaces pourraient être réduites à des tables de fonctions virtuelles ou à des appels directs, et on pourrait aussi travailler sur des structures plutôt que sur des maps. On pourrait avoir des types
IntetFloat, rétrogradés enNumbersi nécessaire, tout en les conservant dans les registresLe problème central, c’est que TS et V8 sont tous deux des cibles non standard qui évoluent vite. Ce genre de projet nécessite une grande équipe, et le simple maintien de la compatibilité devient un travail à part entière
Exemple simple : TypeScript ne distingue pas les entiers des nombres à virgule flottante, il traite tout comme des nombres. Du coup, chaque accès à un tableau nécessite une conversion de type. Si TypeScript avait été conçu pour faciliter la compilation statique, cette distinction existerait probablement
Le problème plus important, c’est le sous-typage structurel de TypeScript. À cause de cette propriété, il est pratiquement impossible pour le compilateur de déterminer statiquement la structure physique des arguments non primitifs passés à une fonction. Un JIT peut effectuer une analyse dynamique des formes, donc on risque d’obtenir des performances inférieures au JIT sur chaque accès à un champ
De nombreux travaux ont déjà porté sur des outils d’analyse statique de types pour JS, et il est possible de faire des analyses très poussées. Un exemple qui me vient à l’esprit est TAJS, même si c’est un peu ancien
J’aimerais au moins que TypeScript permette de spécifier des types comme
integer. Même si une compilation TS→JS classique traiteconst val: intexactement commeconst val: number, un runtime moderne capable de comprendre TS pourrait exploiter cette information supplémentaireJe me demande si une syntaxe comme
const counter: Numberpourrait être acceptéeJe ne sais pas si le site a changé, ou si c’est moi qui ai raté quelque chose
Chez windmill.dev, quand les utilisateurs déploient du code, ils utilisent Bun build pour regrouper le script et toutes ses dépendances dans un seul fichier JS, qu’ils chargent ensuite afin d’améliorer le cold start et l’utilisation mémoire. À cause de la taille du bundle, le résultat est stocké sur S3
Si tout pouvait être bundlé nativement, ce serait complètement différent. Même si le cold start de Bun est excellent, il est difficile de rivaliser avec une exécution native directe depuis un petit binaire
J’aime voir davantage de runtimes JS explorer l’accès à Wasm. Ce projet me rappelle Static Hermes, le moteur JS de Facebook destiné à accélérer les performances sur iOS et Android pour les projets React Native
Les deux visent la conformité à JS test262, et Porffor prend en charge à la fois la sortie native et Wasm, tandis que Static Hermes se concentre actuellement surtout sur la sortie native. Porffor est écrit en pur JS et vise à pouvoir se compiler lui-même, alors que Static Hermes dépend de LLVM. La prise en charge de async/promise/await restait encore limitée dans Porffor, tandis que Static Hermes la prend en charge avec certaines restrictions. Static Hermes est écrit en C++, Porffor principalement en JS. Les deux prennent en charge TypeScript, mais Static Hermes transpile l’AST TS vers Flow, alors que Porffor le gère nativement. Static Hermes dispose d’un interpréteur de secours pour les situations JS difficiles à compiler comme
eval, tandis que Porffor ne prend en charge que la compilation anticipéeGlobalement, j’espère que ce projet gagnera en élan et pourra rendre le moteur JavaScript de l’edge plus rapide. Signé Syrus de Wasmer
https://github.com/facebook/hermes/discussions/1137
https://github.com/tc39/test262
https://wasmer.io
Cela dit, ce n’est pas notre priorité, nous nous concentrons surtout sur React Native. Dans cet environnement, WASM n’a pas vraiment d’intérêt
La fonctionnalité la plus importante de Static Hermes est un vérificateur de types qui garantit la solidité à l’exécution. Porffor est très intéressant, je le suis depuis un moment et j’espère qu’il réussira
C’est une approche similaire à Kiesel : https://kiesel.dev/
Array.prototype.filter,Math.sinetatobcompilent déjà en partie elles-mêmesRécemment, Porffor a aussi commencé à prendre en charge les bases de async/promise/await. Cela ne fonctionne pas encore très bien
Il existe dans JavaScript un sous-ensemble facile à compiler, et la difficulté se trouve dans la longue traîne qui reste. Cela dit, c’est formidable de voir des recherches menées sur où se situe cette frontière et sur les gains qu’on peut tirer de ce sous-ensemble
J’aime sincèrement le fait qu’il prenne en charge
String.blink. Le fait qu’un développeur ait de l’humour et un côté joueur est toujours bon signeL’implémentation elle-même est aussi triviale que
function() { return "" + this + ""; }, donc cela vaut la peine de l’implémenter même si l’hôte ECMAScript n’est pas un navigateur web. Dans ce cas, c’est facultatif. Je ne m’attendrais pas à ce que cela ait quoi que ce soit à voir avec de “l’humour ou un côté joueur”String.blinkfigure dans test262, donc si l’on veut atteindre les objectifs du projet, il faut en pratique le prendre en chargeJe me demande quelle nuance subtile m’a échappé. Je ne vois pas pourquoi “moteur JS en compilation anticipée” serait une meilleure description que “compilateur JS-vers-Wasm”. Si c’est surtout une stratégie de cadrage, cela me va aussi
Le système de versioning décrit ici me semble un peu douteux
Si un changement provoque une régression sur certains tests Test262, alors le numéro de version pourrait lui aussi devoir reculer. Autrement dit, Porffor ne peut pas avoir à la fois des numéros de version monotoniquement croissants et la possibilité d’introduire des changements nécessaires qui causent des régressions Test262
https://github.com/CanadaHonk/porffor?tab=readme-ov-file#ver...
Cela signifie “violet” en gallois
C’est rafraîchissant de voir plusieurs moteurs JS apparaître pour des usages variés
Pour embarquer des plugins dans une application, j’ai travaillé à fournir davantage d’API compatibles Node à quickjs via llrt
https://github.com/awslabs/llrt