La cache bytecode è un'ottimizzazione in fase di build che migliora drasticamente il tempo di avvio dell'applicazione pre-compilando il JavaScript in bytecode. Ad esempio, quando si compila tsc di TypeScript con il bytecode abilitato, il tempo di avvio migliora di 2 volte.
Utilizzo
Utilizzo di base
Abilita la cache bytecode con il flag --bytecode:
bun build ./index.ts --target=bun --bytecode --outdir=./distQuesto genera due file:
dist/index.js- Il JavaScript bundlatodist/index.jsc- Il file di cache bytecode
A runtime, Bun rileva e utilizza automaticamente il file .jsc:
bun ./dist/index.js # Usa automaticamente index.jscCon eseguibili standalone
Quando si creano eseguibili con --compile, il bytecode è incorporato nel binario:
bun build ./cli.ts --compile --bytecode --outfile=mycliL'eseguibile risultante contiene sia il codice che il bytecode, offrendo le massime prestazioni in un singolo file.
Combinazione con altre ottimizzazioni
Il bytecode funziona benissimo con minificazione e source map:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyriduce la dimensione del codice prima di generare il bytecode (meno codice -> meno bytecode)--sourcemappreserva la segnalazione degli errori (gli errori puntano ancora alla sorgente originale)--bytecodeelimina il sovraccarico di parsing
Impatto sulle prestazioni
Il miglioramento delle prestazioni scala con la dimensione del codebase:
| Dimensione applicazione | Tipico miglioramento avvio |
|---|---|
| CLI piccola (< 100 KB) | 1,5-2 volte più veloce |
| App medio-grande (> 5 MB) | 2,5-4 volte più veloce |
Le applicazioni più grandi beneficiano di più perché hanno più codice da analizzare.
Quando usare il bytecode
Ideale per:
Strumenti CLI
- Invocati frequentemente (linter, formattatori, git hooks)
- Il tempo di avvio è l'intera esperienza utente
- Gli utenti notano la differenza tra 90ms e 45ms di avvio
- Esempio: compilatore TypeScript, Prettier, ESLint
Strumenti di build e task runner
- Eseguiti centinaia o migliaia di volte durante lo sviluppo
- I millisecondi risparmiati per esecuzione si accumulano rapidamente
- Miglioramento dell'esperienza dello sviluppatore
- Esempio: script di build, test runner, generatori di codice
Eseguibili standalone
- Distribuiti a utenti che tengono alle prestazioni scattanti
- La distribuzione in file singolo è conveniente
- La dimensione del file è meno importante del tempo di avvio
- Esempio: CLI distribuite via npm o come binari
Evitarlo per:
- ❌ Script piccoli
- ❌ Codice eseguito una volta
- ❌ Build di sviluppo
- ❌ Ambienti con vincoli di dimensione
- ❌ Codice con await di primo livello (non supportato)
Limitazioni
Solo CommonJS
La cache bytecode funziona attualmente con il formato di output CommonJS. Il bundler di Bun converte automaticamente la maggior parte del codice ESM in CommonJS, ma await di primo livello è l'eccezione:
// Questo impedisce la cache bytecode
const data = await fetch("https://api.example.com");
export default data;Perché: L'await di primo livello richiede la valutazione asincrona del modulo, che non può essere rappresentata in CommonJS. Il grafo dei moduli diventa asincrono e il modello della funzione wrapper CommonJS non funziona.
Soluzione alternativa: Sposta l'inizializzazione asincrona in una funzione:
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;Ora il modulo esporta una funzione che il consumatore può attendere quando necessario.
Compatibilità delle versioni
Il bytecode non è portabile tra le versioni di Bun. Il formato bytecode è legato alla rappresentazione interna di JavaScriptCore, che cambia tra le versioni.
Quando aggiorni Bun, devi rigenerare il bytecode:
# Dopo aver aggiornato Bun
bun build --bytecode ./index.ts --outdir=./distSe il bytecode non corrisponde alla versione corrente di Bun, viene automaticamente ignorato e il codice torna al parsing della sorgente JavaScript. La tua app continua a funzionare - perdi solo l'ottimizzazione delle prestazioni.
Best practice: Genera il bytecode come parte del processo di build CI/CD. Non fare commit dei file .jsc su git. Rigenerali ogni volta che aggiorni Bun.
Il codice sorgente è ancora necessario
- Il file
.js(il codice sorgente bundlato) - Il file
.jsc(la cache bytecode)
A runtime:
- Bun carica il file
.js, vede un pragma@bytecodee controlla il file.jsc - Bun carica il file
.jsc - Bun valida che l'hash del bytecode corrisponda alla sorgente
- Se valido, Bun usa il bytecode
- Se non valido, Bun torna al parsing della sorgente
Il bytecode non è offuscamento
Il bytecode non oscura il codice sorgente. È un'ottimizzazione, non una misura di sicurezza.
Distribuzione in produzione
Docker
Includi la generazione del bytecode nel tuo 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"]Il bytecode è indipendente dall'architettura.
CI/CD
Genera il bytecode durante la tua pipeline di build:
# GitHub Actions
- name: Build con bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsDebug
Verificare che il bytecode venga utilizzato
Controlla che il file .jsc esista:
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jscIl file .jsc dovrebbe essere 2-8 volte più grande del file .js.
Per registrare se il bytecode viene utilizzato, imposta BUN_JSC_verboseDiskCache=1 nel tuo ambiente.
In caso di successo, registrerà qualcosa come:
[Disk cache] cache hit for sourceCodeSe vedi un cache miss, registrerà qualcosa come:
[Disk cache] cache miss for sourceCodeÈ normale che registri un cache miss più volte poiché Bun attualmente non memorizza nella cache bytecode il codice JavaScript utilizzato nei moduli built-in.
Problemi comuni
Bytecode ignorato silenziosamente: Solitamente causato da un aggiornamento della versione di Bun. La versione della cache non corrisponde, quindi il bytecode viene rifiutato. Rigenera per risolvere.
Dimensione file troppo grande: Questo è previsto. Considera:
- Usare
--minifyper ridurre la dimensione del codice prima della generazione del bytecode - Comprimere i file
.jscper il trasferimento di rete (gzip/brotli) - Valutare se il guadagno di prestazioni all'avvio vale l'aumento di dimensione
Await di primo livello: Non supportato. Ristruttura per usare funzioni di inizializzazione asincrone.
Cos'è il bytecode?
Quando esegui JavaScript, il motore JavaScript non esegue direttamente il codice sorgente. Invece, passa attraverso diversi passaggi:
- Parsing: Il motore legge il codice sorgente JavaScript e lo converte in un Abstract Syntax Tree (AST)
- Compilazione bytecode: L'AST viene compilato in bytecode - una rappresentazione di livello inferiore più veloce da eseguire
- Esecuzione: Il bytecode viene eseguito dall'interprete o dal compilatore JIT del motore
Il bytecode è una rappresentazione intermedia - è di livello inferiore rispetto al codice sorgente JavaScript, ma di livello superiore rispetto al codice macchina. Pensalo come linguaggio assembly per una macchina virtuale. Ogni istruzione bytecode rappresenta una singola operazione come "carica questa variabile", "aggiungi due numeri" o "chiama questa funzione".
Questo accade ogni singola volta che esegui il codice. Se hai uno strumento CLI eseguito 100 volte al giorno, il codice viene analizzato 100 volte. Se hai una funzione serverless con cold start frequenti, il parsing avviene ad ogni cold start.
Con la cache bytecode, Bun sposta i passaggi 1 e 2 alla fase di build. A runtime, il motore carica il bytecode pre-compilato e passa direttamente all'esecuzione.
Perché il lazy parsing rende tutto ancora migliore
I moderni motori JavaScript usano un'ottimizzazione intelligente chiamata lazy parsing. Non analizzano tutto il codice in anticipo - invece, le funzioni vengono analizzate solo quando vengono chiamate per la prima volta:
// Senza cache bytecode:
function rarely_used() {
// Questa funzione di 500 righe viene analizzata solo
// quando viene effettivamente chiamata
}
function main() {
console.log("Avvio app");
// rarely_used() non viene mai chiamata, quindi non viene mai analizzata
}Questo significa che il sovraccarico di parsing non è solo un costo di avvio - accade durante l'intero ciclo di vita dell'applicazione mentre vengono eseguiti diversi percorsi di codice. Con la cache bytecode, tutte le funzioni sono pre-compilate, anche quelle analizzate pigramente. Il lavoro di parsing avviene una volta in fase di build invece di essere distribuito durante l'esecuzione dell'applicazione.
Il formato bytecode
Dentro un file .jsc
Un file .jsc contiene una struttura bytecode serializzata. Capire cosa c'è dentro aiuta a spiegare sia i vantaggi delle prestazioni che il compromesso della dimensione del file.
Sezione header (validata ad ogni caricamento):
- Versione cache: Un hash legato alla versione del framework JavaScriptCore. Questo assicura che il bytecode generato con una versione di Bun venga eseguito solo con quella versione esatta.
- Tag tipo blocco codice: Identifica se si tratta di un blocco codice Program, Module, Eval o Function.
SourceCodeKey (valida che il bytecode corrisponda alla sorgente):
- Hash codice sorgente: Un hash del codice sorgente JavaScript originale. Bun verifica che corrisponda prima di usare il bytecode.
- Lunghezza codice sorgente: La lunghezza esatta della sorgente, per validazione aggiuntiva.
- Flag di compilazione: Contesto di compilazione critico come strict mode, se è uno script vs modulo, tipo contesto eval, ecc. Lo stesso codice sorgente compilato con flag diversi produce bytecode diverso.
Istruzioni bytecode:
- Flusso istruzioni: Gli opcode bytecode effettivi - la rappresentazione compilata del JavaScript. Questa è una sequenza di istruzioni bytecode a lunghezza variabile.
- Tabella metadati: Ogni opcode ha metadati associati - cose come contatori di profiling, suggerimenti di tipo e conteggi di esecuzione (anche se non ancora popolati).
- Target jump: Indirizzi pre-calcolati per il flusso di controllo (if/else, loop, istruzioni switch).
- Tabelle switch: Tabelle di lookup ottimizzate per istruzioni switch.
Costanti e identificatori:
- Pool costanti: Tutti i valori letterali nel codice - numeri, stringhe, booleani, null, undefined. Questi sono memorizzati come valori JavaScript effettivi (JSValues) così non devono essere analizzati dalla sorgente a runtime.
- Tabella identificatori: Tutti i nomi di variabili e funzioni usati nel codice. Memorizzati come stringhe deduplicate.
- Marcatori rappresentazione codice sorgente: Flag che indicano come le costanti dovrebbero essere rappresentate (come interi, double, big int, ecc.).
Metadati funzione (per ogni funzione nel codice):
- Allocazione registri: Quanti registri (variabili locali) la funzione necessita -
thisRegister,scopeRegister,numVars,numCalleeLocals,numParameters. - Caratteristiche codice: Un bitmask di caratteristiche funzione: è un costruttore? una funzione freccia? usa
super? ha tail calls? Queste influenzano come la funzione viene eseguita. - Caratteristiche scope lessicale: Strict mode e altro contesto lessicale.
- Modalità parsing: La modalità in cui la funzione è stata analizzata (normal, async, generator, async generator).
Strutture nidificate:
- Dichiarazioni ed espressioni funzione: Ogni funzione nidificata ottiene il proprio blocco bytecode, ricorsivamente. Un file con 100 funzioni ha 100 blocchi bytecode separati, tutti nidificati nella struttura.
- Gestori eccezioni: Blocchi Try/catch/finally con i loro confini e indirizzi gestore pre-calcolati.
- Info espressione: Mappa le posizioni bytecode alle posizioni codice sorgente per segnalazione errori e debug.
Cosa NON contiene il bytecode
Importante, il bytecode non incorpora il codice sorgente. Invece:
- La sorgente JavaScript è memorizzata separatamente (nel file
.js) - Il bytecode memorizza solo un hash e la lunghezza della sorgente
- Al momento del caricamento, Bun valida che il bytecode corrisponda al codice sorgente corrente
Questo è il motivo per cui devi distribuire sia i file .js che .jsc. Il file .jsc è inutile senza il suo corrispondente file .js.
Il compromesso: dimensione file
I file bytecode sono significativamente più grandi del codice sorgente - tipicamente 2-8 volte più grandi.
Perché il bytecode è così molto più grande?
Le istruzioni bytecode sono verbose: Una singola riga di JavaScript minificato potrebbe compilare in dozzine di istruzioni bytecode. Per esempio:
const sum = arr.reduce((a, b) => a + b, 0);Compila in bytecode che:
- Carica la variabile
arr - Ottiene la proprietà
reduce - Crea la funzione freccia (che ha essa stessa bytecode)
- Carica il valore iniziale
0 - Imposta la chiamata con il numero corretto di argomenti
- Esegue effettivamente la chiamata
- Memorizza il risultato in
sum
Ognuno di questi passaggi è un'istruzione bytecode separata con i propri metadati.
I pool costanti memorizzano tutto: Ogni stringa letterale, numero, nome proprietà - tutto viene memorizzato nel pool costanti. Anche se il codice sorgente ha "hello" cento volte, il pool costanti lo memorizza una volta, ma la tabella identificatori e i riferimenti costanti aggiungono sovraccarico.
Metadati per funzione: Ogni funzione - anche piccole funzioni di una riga - ottiene i propri metadati completi:
- Info allocazione registri
- Bitmask caratteristiche codice
- Modalità parsing
- Gestori eccezioni
- Info espressione per debug
Un file con 1.000 piccole funzioni ha 1.000 set di metadati.
Strutture dati profiling: Anche se i dati di profiling non sono ancora popolati, le strutture per contenere i dati di profiling sono allocate. Questo include:
- Slot profilo valore (tracciano quali tipi fluiscono attraverso ogni operazione)
- Slot profilo array (tracciano pattern di accesso array)
- Slot profilo aritmetico binario (tracciano tipi numero in operazioni matematiche)
- Slot profilo aritmetico unario
Questi occupano spazio anche quando vuoti.
Flusso controllo pre-calcolato: Target jump, tabelle switch e confini gestori eccezioni sono tutti pre-calcolati e memorizzati. Questo rende l'esecuzione più veloce ma aumenta la dimensione del file.
Strategie di mitigazione
Compressione: Il bytecode si comprime estremamente bene con gzip/brotli (compressione 60-70%). La struttura ripetitiva e i metadati si comprimono efficientemente.
Prima la minificazione: Usare --minify prima della generazione bytecode aiuta:
- Identificatori più corti → tabella identificatori più piccola
- Eliminazione codice morto → meno bytecode generato
- Constant folding → meno costanti nel pool
Il compromesso: Stai scambiando file 2-4 volte più grandi per avvio 2-4 volte più veloce. Per le CLI, questo di solito vale la pena. Per server a lunga esecuzione dove qualche megabyte di spazio su disco non importa, è ancora meno un problema.
Versioning e portabilità
Portabilità cross-architettura: ✅
Il bytecode è indipendente dall'architettura. Puoi:
- Costruire su macOS ARM64, distribuire su Linux x64
- Costruire su Linux x64, distribuire su AWS Lambda ARM64
- Costruire su Windows x64, distribuire su macOS ARM64
Il bytecode contiene istruzioni astratte che funzionano su qualsiasi architettura. Le ottimizzazioni specifiche dell'architettura avvengono durante la compilazione JIT a runtime, non nel bytecode memorizzato nella cache.
Portabilità cross-versione: ❌
Il bytecode non è stabile tra le versioni di Bun. Ecco perché:
Il formato bytecode cambia: Il formato bytecode di JavaScriptCore evolve. Nuovi opcode vengono aggiunti, quelli vecchi vengono rimossi o modificati, le strutture metadati cambiano. Ogni versione di JavaScriptCore ha un formato bytecode diverso.
Validazione versione: La versione cache nell'header del file .jsc è un hash del framework JavaScriptCore. Quando Bun carica il bytecode:
- Estrae la versione cache dal file
.jsc - Calcola la versione corrente di JavaScriptCore
- Se non corrispondono, il bytecode viene silenziosamente rifiutato
- Bun torna al parsing della sorgente
.js
La tua applicazione continua a funzionare - perdi solo l'ottimizzazione delle prestazioni.
Degradazione graduale: Questo design significa che la cache bytecode "fallisce in apertura" - se qualcosa va storto (mancata corrispondenza versione, file corrotto, file mancante), il codice continua a funzionare normalmente. Potresti vedere un avvio più lento, ma non vedrai errori.
Bytecode non collegato vs collegato
JavaScriptCore fa una distinzione cruciale tra bytecode "non collegato" e "collegato". Questa separazione è ciò che rende possibile la cache bytecode:
Bytecode non collegato (ciò che viene memorizzato nella cache)
Il bytecode salvato nei file .jsc è bytecode non collegato. Contiene:
- Le istruzioni bytecode compilate
- Informazioni strutturali sul codice
- Costanti e identificatori
- Informazioni flusso controllo
Ma non contiene:
- Puntatori a oggetti runtime effettivi
- Codice macchina compilato JIT
- Dati profiling da esecuzioni precedenti
- Informazioni link chiamate (quali funzioni chiamano quali)
Il bytecode non collegato è immutabile e condivisibile. Multiple esecuzioni dello stesso codice possono tutte fare riferimento allo stesso bytecode non collegato.
Bytecode collegato (esecuzione runtime)
Quando Bun esegue il bytecode, lo "collega" - creando un wrapper runtime che aggiunge:
- Informazioni link chiamate: Mentre il codice viene eseguito, il motore apprende quali funzioni chiamano quali e ottimizza quei siti di chiamata.
- Dati profiling: Il motore traccia quante volte ogni istruzione viene eseguita, quali tipi di valori fluiscono attraverso il codice, pattern di accesso array, ecc.
- Stato compilazione JIT: Riferimenti a versioni compilate JIT baseline o ottimizzanti (DFG/FTL) di codice hot.
- Oggetti runtime: Puntatori a oggetti JavaScript effettivi, prototipi, scope, ecc.
Questa rappresentazione collegata viene creata da zero ogni volta che esegui il codice. Questo permette:
- Memorizzare nella cache il lavoro costoso (parsing e compilazione in bytecode non collegato)
- Continuare a raccogliere dati profiling runtime per guidare le ottimizzazioni
- Continuare ad applicare ottimizzazioni JIT basate sui pattern di esecuzione effettivi
La cache bytecode sposta il lavoro costoso (parsing e compilazione in bytecode) da runtime a build time. Per applicazioni che si avviano frequentemente, questo può dimezzare il tempo di avvio al costo di file più grandi su disco.
Per CLI di produzione e distribuzioni serverless, la combinazione di --bytecode --minify --sourcemap offre le migliori prestazioni mantenendo la debuggabilità.