La mise en cache du bytecode est une optimisation au moment de la construction qui améliore considérablement le temps de démarrage de l'application en pré-compilant votre JavaScript en bytecode. Par exemple, lors de la compilation de tsc de TypeScript avec le bytecode activé, le temps de démarrage s'améliore de 2x.
Utilisation
Utilisation de base
Activez la mise en cache du bytecode avec l'option --bytecode :
bun build ./index.ts --target=bun --bytecode --outdir=./distCela génère deux fichiers :
dist/index.js- Votre JavaScript bundledist/index.jsc- Le fichier de cache du bytecode
Au moment de l'exécution, Bun détecte et utilise automatiquement le fichier .jsc :
bun ./dist/index.js # Utilise automatiquement index.jscAvec les exécutables autonomes
Lors de la création d'exécutables avec --compile, le bytecode est intégré dans le binaire :
bun build ./cli.ts --compile --bytecode --outfile=mycliL'exécutable résultant contient à la fois le code et le bytecode, vous offrant des performances maximales dans un seul fichier.
Combinaison avec d'autres optimisations
Le bytecode fonctionne très bien avec la minification et les source maps :
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyréduit la taille du code avant de générer le bytecode (moins de code -> moins de bytecode)--sourcemappréserve le signalement des erreurs (les erreurs pointent toujours vers la source originale)--bytecodeélimine la surcharge d'analyse
Impact sur les performances
L'amélioration des performances évolue avec la taille de votre base de code :
| Taille de l'application | Amélioration typique du démarrage |
|---|---|
| Petit CLI (< 100 Ko) | 1,5-2x plus rapide |
| Grande application (> 5 Mo) | 2,5x-4x plus rapide |
Les applications plus grandes bénéficient davantage car elles ont plus de code à analyser.
Quand utiliser le bytecode
Idéal pour :
Outils CLI
- Invoqués fréquemment (linters, formatters, git hooks)
- Le temps de démarrage constitue toute l'expérience utilisateur
- Les utilisateurs remarquent la différence entre 90ms et 45ms de démarrage
- Exemple : compilateur TypeScript, Prettier, ESLint
Outils de construction et exécutants de tâches
- S'exécutent des centaines ou des milliers de fois pendant le développement
- Les millisecondes économisées par exécution s'accumulent rapidement
- Amélioration de l'expérience développeur
- Exemple : scripts de construction, exécutants de tests, générateurs de code
Exécutables autonomes
- Distribués aux utilisateurs qui se soucient des performances réactives
- La distribution en un seul fichier est pratique
- La taille du fichier est moins importante que le temps de démarrage
- Exemple : CLIs distribués via npm ou en tant que binaires
À éviter pour :
- ❌ Petits scripts
- ❌ Code qui s'exécute une fois
- ❌ Constructions de développement
- ❌ Environnements avec contraintes de taille
- ❌ Code avec await de haut niveau (non pris en charge)
Limitations
CommonJS uniquement
La mise en cache du bytecode fonctionne actuellement avec le format de sortie CommonJS. Le bundler de Bun convertit automatiquement la plupart du code ESM en CommonJS, mais l'await de haut niveau est l'exception :
// Cela empêche la mise en cache du bytecode
const data = await fetch("https://api.example.com");
export default data;Pourquoi : L'await de haut niveau nécessite une évaluation asynchrone du module, qui ne peut pas être représentée en CommonJS. Le graphe de modules devient asynchrone et le modèle de fonction wrapper CommonJS ne fonctionne plus.
Solution de contournement : Déplacez l'initialisation asynchrone dans une fonction :
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;Maintenant, le module exporte une fonction que le consommateur peut attendre si nécessaire.
Compatibilité des versions
Le bytecode n'est pas portable entre les versions de Bun. Le format du bytecode est lié à la représentation interne de JavaScriptCore, qui change entre les versions.
Lorsque vous mettez à jour Bun, vous devez régénérer le bytecode :
# Après la mise à jour de Bun
bun build --bytecode ./index.ts --outdir=./distSi le bytecode ne correspond pas à la version actuelle de Bun, il est automatiquement ignoré et votre code revient à l'analyse de la source JavaScript. Votre application s'exécute toujours - vous perdez simplement l'optimisation des performances.
Meilleure pratique : Générez le bytecode dans le cadre de votre processus CI/CD. Ne validez pas les fichiers .jsc dans git. Régénérez-les chaque fois que vous mettez à jour Bun.
Code source toujours requis
- Le fichier
.js(votre code source bundle) - Le fichier
.jsc(le fichier de cache du bytecode)
Au moment de l'exécution :
- Bun charge le fichier
.js, voit un pragma@bytecodeet vérifie le fichier.jsc - Bun charge le fichier
.jsc - Bun valide que le hachage du bytecode correspond à la source
- Si valide, Bun utilise le bytecode
- Si invalide, Bun revient à l'analyse de la source
Le bytecode n'est pas une obfuscation
Le bytecode n'obscurcit pas votre code source. C'est une optimisation, pas une mesure de sécurité.
Déploiement en production
Docker
Incluez la génération du bytecode dans votre Dockerfile :
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]Le bytecode est indépendant de l'architecture.
CI/CD
Générez le bytecode pendant votre pipeline de construction :
# GitHub Actions
- name: Build with bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsDébogage
Vérifier que le bytecode est utilisé
Vérifiez que le fichier .jsc existe :
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jscLe fichier .jsc devrait être 2 à 8 fois plus grand que le fichier .js.
Pour journaliser si le bytecode est utilisé, définissez BUN_JSC_verboseDiskCache=1 dans votre environnement.
En cas de succès, cela journalisera quelque chose comme :
[Disk cache] cache hit for sourceCodeSi vous voyez un cache miss, cela journalisera quelque chose comme :
[Disk cache] cache miss for sourceCodeIl est normal qu'il journalise un cache miss plusieurs fois car Bun ne met pas actuellement en cache le bytecode du code JavaScript utilisé dans les modules intégrés.
Problèmes courants
Bytecode ignoré silencieusement : Généralement causé par une mise à jour de Bun. La version du cache ne correspond pas, donc le bytecode est rejeté. Régénérez pour corriger.
Taille de fichier trop grande : C'est attendu. Envisagez :
- Utiliser
--minifypour réduire la taille du code avant la génération du bytecode - Compresser les fichiers
.jscpour le transfert réseau (gzip/brotli) - Évaluer si le gain de performance au démarrage vaut l'augmentation de taille
Await de haut niveau : Non pris en charge. Refactorisez pour utiliser des fonctions d'initialisation asynchrones.
Qu'est-ce que le bytecode ?
Lorsque vous exécutez du JavaScript, le moteur JavaScript n'exécute pas votre code source directement. Au lieu de cela, il passe par plusieurs étapes :
- Analyse : Le moteur lit votre code source JavaScript et le convertit en un arbre syntaxique abstrait (AST)
- Compilation du bytecode : L'AST est compilé en bytecode - une représentation de niveau inférieur qui est plus rapide à exécuter
- Exécution : Le bytecode est exécuté par l'interpréteur du moteur ou le compilateur JIT
Le bytecode est une représentation intermédiaire - c'est un niveau inférieur au code source JavaScript, mais un niveau supérieur au code machine. Considérez-le comme un langage d'assemblage pour une machine virtuelle. Chaque instruction de bytecode représente une seule opération comme « charger cette variable », « ajouter deux nombres » ou « appeler cette fonction ».
Cela se produit à chaque fois que vous exécutez votre code. Si vous avez un outil CLI qui s'exécute 100 fois par jour, votre code est analysé 100 fois. Si vous avez une fonction serverless avec des démarrages à froid fréquents, l'analyse se produit à chaque démarrage à froid.
Avec la mise en cache du bytecode, Bun déplace les étapes 1 et 2 vers l'étape de construction. Au moment de l'exécution, le moteur charge le bytecode pré-compilé et passe directement à l'exécution.
Pourquoi l'analyse paresseuse rend cela encore meilleur
Les moteurs JavaScript modernes utilisent une optimisation astucieuse appelée analyse paresseuse. Ils n'analysent pas tout votre code d'emblée - au lieu de cela, les fonctions sont uniquement analysées lorsqu'elles sont appelées pour la première fois :
// Sans mise en cache du bytecode :
function rarely_used() {
// Cette fonction de 500 lignes est uniquement analysée
// lorsqu'elle est réellement appelée
}
function main() {
console.log("Démarrage de l'application");
// rarely_used() n'est jamais appelée, donc elle n'est jamais analysée
}Cela signifie que la surcharge d'analyse n'est pas seulement un coût de démarrage - elle se produit tout au long de la durée de vie de votre application lorsque différents chemins de code s'exécutent. Avec la mise en cache du bytecode, toutes les fonctions sont pré-compilées, même celles qui sont analysées de manière paresseuse. Le travail d'analyse se produit une fois au moment de la construction au lieu d'être distribué tout au long de l'exécution de votre application.
Le format du bytecode
À l'intérieur d'un fichier .jsc
Un fichier .jsc contient une structure de bytecode sérialisée. Comprendre ce qui se trouve à l'intérieur aide à expliquer à la fois les avantages de performance et le compromis de taille de fichier.
Section d'en-tête (validée à chaque chargement) :
- Version du cache : Un hachage lié à la version du framework JavaScriptCore. Cela garantit que le bytecode généré avec une version de Bun s'exécute uniquement avec cette version exacte.
- Balise de type de bloc de code : Identifie s'il s'agit d'un bloc de code Program, Module, Eval ou Function.
SourceCodeKey (valide que le bytecode correspond à la source) :
- Hachage du code source : Un hachage du code source JavaScript original. Bun vérifie que cela correspond avant d'utiliser le bytecode.
- Longueur du code source : La longueur exacte de la source, pour validation supplémentaire.
- Indicateurs de compilation : Contexte de compilation critique comme le mode strict, s'il s'agit d'un script vs module, type de contexte eval, etc. Le même code source compilé avec différents indicateurs produit un bytecode différent.
Instructions de bytecode :
- Flux d'instructions : Les opcodes de bytecode réels - la représentation compilée de votre JavaScript. Il s'agit d'une séquence de longueur variable d'instructions de bytecode.
- Table de métadonnées : Chaque opcode a des métadonnées associées - des choses comme des compteurs de profilage, des indices de type et des compteurs d'exécution (même s'ils ne sont pas encore remplis).
- Cibles de saut : Adresses pré-calculées pour le flux de contrôle (if/else, boucles, instructions switch).
- Tables de switch : Tables de recherche optimisées pour les instructions switch.
Constantes et identifiants :
- Pool de constantes : Toutes les valeurs littérales dans votre code - nombres, chaînes, booléens, null, undefined. Ceux-ci sont stockés en tant que valeurs JavaScript réelles (JSValues) afin qu'elles n'aient pas besoin d'être analysées à partir de la source au moment de l'exécution.
- Table d'identifiants : Tous les noms de variables et de fonctions utilisés dans le code. Stockés en tant que chaînes dédupliquées.
- Marqueurs de représentation du code source : Indicateurs indiquant comment les constantes doivent être représentées (en tant qu'entiers, doubles, grands entiers, etc.).
Métadonnées de fonction (pour chaque fonction dans votre code) :
- Allocation de registres : Combien de registres (variables locales) la fonction nécessite -
thisRegister,scopeRegister,numVars,numCalleeLocals,numParameters. - Fonctionnalités du code : Un masque de bits des caractéristiques de la fonction : est-ce un constructeur ? une fonction fléchée ? utilise-t-elle
super? a-t-elle des appels de queue ? Ceux-ci affectent la façon dont la fonction est exécutée. - Fonctionnalités à portée lexicale : Mode strict et autre contexte lexical.
- Mode d'analyse : Le mode dans lequel la fonction a été analysée (normal, async, générateur, async générateur).
Structures imbriquées :
- Déclarations et expressions de fonction : Chaque fonction imbriquée obtient son propre bloc de bytecode, récursivement. Un fichier avec 100 fonctions a 100 blocs de bytecode séparés, tous imbriqués dans la structure.
- Gestionnaires d'exceptions : Blocs try/catch/finally avec leurs limites et adresses de gestionnaire pré-calculées.
- Infos d'expression : Mappe les positions de bytecode vers les emplacements de code source pour le signalement des erreurs et le débogage.
Ce que le bytecode NE contient PAS
De manière importante, le bytecode n'intègre pas votre code source. Au lieu de cela :
- La source JavaScript est stockée séparément (dans le fichier
.js) - Le bytecode stocke uniquement un hachage et une longueur de la source
- Au moment du chargement, Bun valide que le bytecode correspond au code source actuel
C'est pourquoi vous devez déployer à la fois les fichiers .js et .jsc. Le fichier .jsc est inutile sans son fichier .js correspondant.
Le compromis : taille du fichier
Les fichiers de bytecode sont considérablement plus grands que le code source - généralement 2 à 8 fois plus grands.
Pourquoi le bytecode est-il si grand ?
Les instructions de bytecode sont verbeuses : Une seule ligne de JavaScript minifié peut compiler en dizaines d'instructions de bytecode. Par exemple :
const sum = arr.reduce((a, b) => a + b, 0);Compile en bytecode qui :
- Charge la variable
arr - Obtient la propriété
reduce - Crée la fonction fléchée (qui a elle-même du bytecode)
- Charge la valeur initiale
0 - Configure l'appel avec le bon nombre d'arguments
- Effectue réellement l'appel
- Stocke le résultat dans
sum
Chacune de ces étapes est une instruction de bytecode séparée avec ses propres métadonnées.
Les pools de constantes stockent tout : Chaque littéral de chaîne, nombre, nom de propriété - tout est stocké dans le pool de constantes. Même si votre code source a "hello" cent fois, le pool de constantes le stocke une fois, mais la table d'identifiants et les références de constantes ajoutent une surcharge.
Métadonnées par fonction : Chaque fonction - même les petites fonctions d'une ligne - obtient ses propres métadonnées complètes :
- Infos d'allocation de registres
- Masque de bits des fonctionnalités du code
- Mode d'analyse
- Gestionnaires d'exceptions
- Infos d'expression pour le débogage
Un fichier avec 1 000 petites fonctions a 1 000 ensembles de métadonnées.
Structures de données de profilage : Même si les données de profilage ne sont pas encore remplies, les structures pour contenir les données de profilage sont allouées. Cela inclut :
- Emplacements de profil de valeur (suivi des types qui circulent dans chaque opération)
- Emplacements de profil de tableau (suivi des modèles d'accès aux tableaux)
- Emplacements de profil arithmétique binaire (suivi des types de nombres dans les opérations mathématiques)
- Emplacements de profil arithmétique unaire
Cela prend de la place même lorsqu'il est vide.
Flux de contrôle pré-calculé : Les cibles de saut, les tables de switch et les limites des gestionnaires d'exceptions sont tous pré-calculés et stockés. Cela rend l'exécution plus rapide mais augmente la taille du fichier.
Stratégies d'atténuation
Compression : Le bytecode se compresse extrêmement bien avec gzip/brotli (compression de 60-70%). La structure répétitive et les métadonnées se compressent efficacement.
Minification d'abord : Utiliser --minify avant la génération du bytecode aide :
- Identifiants plus courts → table d'identifiants plus petite
- Élimination du code mort → moins de bytecode généré
- Pliage de constantes → moins de constantes dans le pool
Le compromis : Vous échangez des fichiers 2 à 4 fois plus grands contre un démarrage 2 à 4 fois plus rapide. Pour les CLIs, cela en vaut généralement la peine. Pour les serveurs de longue durée où quelques mégaoctets d'espace disque n'ont pas d'importance, c'est encore moins un problème.
Versionnement et portabilité
Portabilité entre architectures : ✅
Le bytecode est indépendant de l'architecture. Vous pouvez :
- Construire sur macOS ARM64, déployer sur Linux x64
- Construire sur Linux x64, déployer sur AWS Lambda ARM64
- Construire sur Windows x64, déployer sur macOS ARM64
Le bytecode contient des instructions abstraites qui fonctionnent sur n'importe quelle architecture. Les optimisations spécifiques à l'architecture se produisent pendant la compilation JIT au moment de l'exécution, pas dans le bytecode mis en cache.
Portabilité entre versions : ❌
Le bytecode n'est pas stable entre les versions de Bun. Voici pourquoi :
Le format de bytecode change : Le format de bytecode de JavaScriptCore évolue. De nouveaux opcodes sont ajoutés, les anciens sont supprimés ou modifiés, les structures de métadonnées changent. Chaque version de JavaScriptCore a un format de bytecode différent.
Validation de version : La version du cache dans l'en-tête du fichier .jsc est un hachage du framework JavaScriptCore. Lorsque Bun charge le bytecode :
- Il extrait la version du cache du fichier
.jsc - Il calcule la version actuelle de JavaScriptCore
- Si elles ne correspondent pas, le bytecode est silencieusement rejeté
- Bun revient à l'analyse du code source
.js
Votre application s'exécute toujours - vous perdez simplement l'optimisation des performances.
Dégradation gracieuse : Cette conception signifie que la mise en cache du bytecode « échoue ouvertement » - si quelque chose ne va pas (incompatibilité de version, fichier corrompu, fichier manquant), votre code s'exécute toujours normalement. Vous pouvez voir un démarrage plus lent, mais vous ne verrez pas d'erreurs.
Bytecode non lié vs lié
JavaScriptCore fait une distinction cruciale entre le bytecode « non lié » et « lié ». Cette séparation est ce qui rend la mise en cache du bytecode possible :
Bytecode non lié (ce qui est mis en cache)
Le bytecode enregistré dans les fichiers .jsc est du bytecode non lié. Il contient :
- Les instructions de bytecode compilées
- Des informations structurelles sur le code
- Des constantes et des identifiants
- Des informations sur le flux de contrôle
Mais il ne contient pas :
- Des pointeurs vers des objets d'exécution réels
- Du code machine compilé JIT
- Des données de profilage des exécutions précédentes
- Des informations de lien d'appel (quelles fonctions appellent lesquelles)
Le bytecode non lié est immuable et partageable. Plusieurs exécutions du même code peuvent toutes référencer le même bytecode non lié.
Bytecode lié (exécution au moment de l'exécution)
Lorsque Bun exécute le bytecode, il le « lie » - créant un wrapper d'exécution qui ajoute :
- Informations de lien d'appel : Au fur et à mesure que votre code s'exécute, le moteur apprend quelles fonctions appellent lesquelles et optimise ces sites d'appel.
- Données de profilage : Le moteur suit le nombre de fois que chaque instruction s'exécute, quels types de valeurs circulent dans le code, les modèles d'accès aux tableaux, etc.
- État de compilation JIT : Références aux versions compilées JIT de base ou optimisées (DFG/FTL) du code chaud.
- Objets d'exécution : Pointeurs vers des objets JavaScript réels, des prototypes, des portées, etc.
Cette représentation liée est créée à chaque fois que vous exécutez votre code. Cela permet :
- Mise en cache du travail coûteux (analyse et compilation en bytecode non lié)
- Collecte de données de profilage d'exécution pour guider les optimisations
- Application d'optimisations JIT basées sur les modèles d'exécution réels
La mise en cache du bytecode déplace le travail coûteux (analyse et compilation en bytecode) du moment de l'exécution au moment de la construction. Pour les applications qui démarrent fréquemment, cela peut réduire de moitié votre temps de démarrage au prix de fichiers plus grands sur le disque.
Pour les CLIs de production et les déploiements serverless, la combinaison de --bytecode --minify --sourcemap vous offre les meilleures performances tout en maintenant la capacité de débogage.