Salta al contenuto principale

Migrazione da Node.js a Bun: gotchas

Articolo della serie Bun Runtime Production 2027. Pubblicazione prevista 16 November 2027.

Cosa ne pensi di questo articolo?

Migrazione da Node.js a Bun: gotchas
Bun Runtime 2027Foto:Brett SaylesPexels

In Breve

Articolo della serie Bun Runtime Production 2027. Pubblicazione prevista 16 November 2027.

Migrazione da Node.js a Bun: I Gotchas Principali

La migrazione da Node.js a Bun è una decisione architettonica importante che promette performance superiori e una developer experience più moderna. Tuttavia, la transizione non è sempre lineare. Bun, sebbene costruito su WebKit JSC (la stessa engine di Safari), introduce differenze significative nel comportamento runtime, nella gestione dei moduli e nella compatibilità con l'ecosistema npm. In questo articolo analizziamo i gotchas più comuni che gli sviluppatori incontrano durante la migrazione, fornendo strategie concrete per evitarli o risolverli.

Basandosi su esperienze di team che hanno già effettuato questa transizione, abbiamo identificato 12 categorie di problemi ricorrenti: differenze nelle API builtin, gestione dei package manager, comportamenti asincroni divergenti, compatibilità delle dipendenze, e configurazione del testing. Ignorare questi aspetti può portare a bug insidiosi che si manifestano solo in produzione, perdita di performance attesa e tempi di debug prolungati.

Cosa Scoprirai in Questo Articolo

  • I 3 gotchas critici che impattano più di altre sulla migrazione
  • Differenze nella gestione della memoria e garbage collection
  • Incompatibilità comuni con librerie npm e come risolverle
  • Strategie di testing con Bun (Bun test vs Jest compatibility)
  • Configurazione di TypeScript e project setup
  • Monitoraggio della performance post-migrazione
  • Quando è opportuno migrare e quando restare su Node.js

Gotcha #1: Package Manager e Lockfile Divergenti

Il primo e più impattante gotcha riguarda la gestione dei package. Bun include un package manager proprietario (bun install) che differisce significativamente da npm e yarn. Il lockfile di Bun (bun.lockb, formato binario) non è compatibile con npm-lock.json.

Quando si migra un progetto Node.js a Bun, occorre scegliere: continuare a usare npm con Bun come runtime, oppure adottare anche bun install come package manager. La scelta ha implicazioni profonde:


# Scenario 1: Bun runtime + npm/yarn package manager
bun install --from=npm    # Converte package-lock.json in bun.lockb
npm install                # Ma poi npm risincronizza il lock

# Scenario 2: Bun runtime + Bun package manager (puro)
rm package-lock.json bun.lockb
bun install                # Crea bun.lockb da scratch
    

Il problema emerge quando il team usa strumenti misti: un developer con npm, un altro con bun install. Questo causa conflitti nel lockfile e risoluzione diversa delle dipendenze transitive. Nelle nostre osservazioni, team che **mantengono npm come package manager ma usano Bun per l'esecuzione** riportano il miglior equilibrio tra compatibility e performance.

Gotcha #2: Moduli CommonJS vs ES Modules

Bun ha un supporto migliore per ES modules rispetto a Node.js, ma anche differenze nel fallback CommonJS. Molte vecchie librerie ancora usano CommonJS, e il comportamento di Bun nel risolvere require() è diverso da Node.js.


// File: utils.mjs (ES module)
export const sum = (a, b) => a + b;

// File: app.js (CommonJS)
// Node.js (works)
const { sum } = require('./utils.mjs');

// Bun (might fail or behave differently)
// Soluzione: usa esplicitamente dynamic import
const { sum } = await import('./utils.mjs');
    

Un altro problema: le librerie che controllano il campo "exports" nel package.json e scelgono condizionalmente CommonJS vs ESM basato su require() vs import. Bun potrebbe scegliere un ramo diverso da Node.js, causando versioni diverse dello stesso modulo nel grafo delle dipendenze.

Gotcha #3: Built-in APIs Divergenti

Bun implementa un sottoinsieme di Node.js APIs, ma anche introduce nuove APIs proprietarie. Le built-in APIs mancanti sono spesso una sorpresa:


// Node.js (works)
import { createReadStream } from 'fs';
const stream = createReadStream('file.txt');
stream.on('data', (chunk) => { ... });

// Bun: Streams funzionano ma il comportamento è leggermente diverso
// Bun preferisce il modello Web Streams (ReadableStream)
import { createReadStream } from 'fs';
const file = Bun.file('file.txt');
const stream = file.stream();

// Altro: Node.js events.EventEmitter
// Bun ha EventTarget nativo (Web API) ma Node.js ha EventEmitter
// Compatibilità: Bun polyfills EventEmitter, ma il comportamento del this binding può differire
    

Variabili d'ambiente, URL resolution, path handling — questi aspetti hanno tutti sottili differenze. Ad esempio, Bun resolve i path relativi rispetto alla root del progetto (dove si trova bunfig.toml), non sempre rispetto al file corrente come Node.js.

Gotcha #4: Comportamento Asincrono e Microtask Queue

Bun e Node.js hanno lifespan leggermente diversi per i microtask. Questo impatta su Promise chain, setTimeout/setImmediate ordering, e cleanup finale dei process.


// Ordine di esecuzione diverso tra Node.js e Bun
Promise.resolve()
  .then(() => console.log('microtask 1'))
  .then(() => console.log('microtask 2'));

setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);

// Node.js ordine: microtask 1, microtask 2, timeout, immediate
// Bun ordine: microtask 1, microtask 2, immediate, timeout
// Nota: comportamento del lifeloop finale è CRITICO
    

Se la tua app si affida all'ordine specifico di event loop phases (setImmediate prima di setTimeout), la migrazione a Bun romperà il comportamento. Questo è particolarmente critico nelle app che chiudono connessioni database o flussi file al termine.

Gotcha #5: Dipendenze Native e Node Addons

Bun NON supporta nativamente i Node.js addons (librerie compilate con node-gyp, come sqlite3, bcrypt, ecc.). Se il tuo progetto dipende da addons compilati, la migrazione a Bun è bloccata fino a quando la libreria non introduce un binding Bun-native.


# Questi funzionano su Node.js (con precompilati binari npm)
npm install sqlite3
npm install bcrypt

# Su Bun, potrebbero fallire:
bun install sqlite3   # Potrebbe compilare con problemi
bun install bcrypt    # Potrebbe non trovar il binding per Bun

# Soluzione: Usa alternative pure JavaScript
# invece di sqlite3: use better-sqlite3 Wasm o bun native bindings (se disponibili)
# invece di bcrypt: prova @noble/hashes (puro JS) per hash, anche se più lento
    

Gotcha #6: Testing Framework e Setup

Bun include un test runner integrato (bun test), ma Jest rimane il gold standard per la compatibilità. Molti progetti ereditano una configurazione Jest consolidata, e migrare al Bun test runner richiede riscrittura dei test.


// File: test/sum.test.js
// Jest style (Node.js)
const { sum } = require('../utils');
describe('sum', () => {
  it('adds two numbers', () => {
    expect(sum(2, 3)).toBe(5);
  });
});

// Bun test style (integrato)
import { describe, it, expect } from 'bun:test';
import { sum } from '../utils';
describe('sum', () => {
  it('adds two numbers', () => {
    expect(sum(2, 3)).toBe(5);
  });
});

// Differenza critica: setup/teardown, mocking, snapshot testing
// Bun ha meno plugin e meno flessibilità di Jest al momento
    

Se usi Jest con plugin complessi (ts-jest, @testing-library, jest-mock-extended), il porting a Bun test non è banale. Raccomando di eseguire entrambi i runner durante la migrazione per validare equivalenza.

Gotcha #7: Gestione della Memoria e Long-Running Processes

Bun usa il garbage collector di WebKit (SamplingHeapProfiler), diverso da V8. In long-running processes (server Express, worker background), il comportamento di memory leak detection è diverso. Memory profile tool e heap snapshot formati non sono compatibili.


# Node.js: Genera heap snapshot V8 (analizzabile con Chrome DevTools)
node --heap-prof app.js

# Bun: Non ha equivalente di heap-prof, memory profiling è manuale
# Soluzione: Monitora via metriche esterne (Prometheus + node-exporter)
# oppure usa Bun built-in memory tracking
console.log('Memory usage:', Bun.memory()); // { rss, heapUsed, heapTotal }
    

Per app con lunga durata (server production), consiglio test di carico prima di adottare Bun. Verificare memory footprint, GC pause time, e comportamento sotto stress.

Gotcha #8: Configurazione Environment e Dotenv

Bun ha un built-in supporto per .env tramite Bun.env, ma differisce dal comportamento di dotenv npm + Node.js:


# Node.js
NODE_ENV=production node app.js

# Bun (load automatico da .env)
bun app.ts

# Ma variabili da riga di comando:
# Node.js: le variabili vanno prima del comando
NODE_ENV=test node app.js

# Bun: specialmente per args bun-specifici, diverso
NODE_ENV=test bun app.ts
    

Gotcha #9: HTTP/HTTPS e Certificati

Bun.serve() è il modo nativo per HTTP, ma le librerie Express/Fastify che usano require('http') funzionano, con comportamento lievemente diverso nel buffering e chunking.


// Express su Node.js
const express = require('express');
const app = express();
app.get('/data', (req, res) => {
  res.json({ message: 'hello' });
});
app.listen(3000);

// Express su Bun (lavora, ma performance inferiore a Bun.serve)
// Soluzione: Usa Bun.serve e porting manuale per massima performance
export default {
  port: 3000,
  async fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === '/data') {
      return Response.json({ message: 'hello' });
    }
    return new Response('404');
  }
};
    

Gotcha #10: Strumenti di Build e Transpilazione

Bun include un transpiler integrato (per TypeScript, JSX), ma gli strumenti di build come Webpack, Vite, ecc. hanno behavior divergenti quando eseguiti su Bun runtime vs Node.js runtime.

Inoltre, se il tuo progetto ha un Makefile o build script che invoca npm script, potrebbe fallire su Bun perché i PATH e i environment variable sono diversi.

Gotcha #11: Debugging e Profiling

Node.js inspector protocol (usato da Chrome DevTools, VS Code debugger) è supportato da Bun solo parzialmente. Debugger integrati potrebbero non funzionare come su Node.js.


# Node.js debugging
node --inspect app.js
# Chrome DevTools si connette a chrome://inspect

# Bun debugging (supported ma meno maturo)
bun --inspect app.ts
# Limited support, diverso da Node.js
    

Gotcha #12: Deploy Pipeline e CI/CD

Se usi GitHub Actions, GitLab CI o altro CI/CD con image Docker Node.js ufficiale, la transizione a Bun richiede cambio dell'immagine base o installazione manuale di Bun. Dockerfile deve essere riscritto.

Strategie di Migrazione Consigliate

Decision Matrix: Quando Migrare a Bun

Scenario Migrazione Consigliata Rischio
App greenfield con zero dipendenze native ✓ Sì Basso
App Node.js stabile con dipendenze native (sqlite3, bcrypt) ✗ No Alto — bloccante
CLI tool semplice (no dipendenze compilate) ✓ Sì Basso
Server Express/Fastify consolidato con large test suite ~ Forse Medio — richiede porting e validazione
Monolite backend con molti microservizi ✗ No Alto — fragmentazione runtime

Checklist di Pre-Migrazione

Prima di decidere la migrazione, esegui questa checklist:

  • Audit delle dipendenze npm: identifica quali usano Node addons (npm list | grep gyp)
  • Verifica supporto Bun di tutte le librerie critiche
  • Setup test parallelizzato: esegui Jest e Bun test allo stesso tempo per delta detection
  • Misura baseline di performance (startup, memory, throughput) su Node.js
  • Identifica criticità nel testing, debugging, deploy
  • Pianifica rollback plan (ci deve essere una via di ritorno facile)

FAQ: Domande Frequenti su Migrazione Bun

D: Bun rimpiazzerà Node.js?
R: Unlikely nel medio termine. Bun è un'ottima alternativa per use case specifici (CLI, script, greenfield), ma Node.js rimane il runtime standard per production web app a causa dell'ecosistema maturo e della stabilità. Bun è una scelta, non un rimpiazzo obbligatorio.

D: Se uso Express su Bun, ottengo davvero il boost di performance?
R: No, non completamente. Il bottleneck diventa il framework Express, non il runtime. Bun shines quando usi Bun.serve() per HTTP server. Se porting a Bun.serve, guadagni 2-5x throughput; se rimani con Express, il gain è marginale (~10-20%).

D: Posso usare Bun solo per alcuni servizi in un monolite?
R: Sì, ma con attenzione. Se uno è Node.js e uno è Bun, dovete gestire comportamenti divergenti, diverse versioni di dipendenze, e complexity operazionale. Consiglio di iniziare con un microservizio isolato (es. una CLI tool o un worker) per validare la soluzione.

D: Quanto tempo per completare una migrazione tipica?
R: Dipende dalla size del codebase e della complexity delle dipendenze. Per un'app piccola (<5k LOC, poche dipendenze): 1-2 settimane. Per un codebase medio (20-50k LOC): 1-2 mesi, con test parallelizzato. Per un'app grande (100k+ LOC, many dependencies): 2-3 mesi, e il rischio aumenta significativamente.

D: Se riscontro un bug durante la migrazione, chi aiuta?
R: La comunità Bun è attiva, ma il supporto commerciale è ancora limitato. Consiglio di documenare bene i bug, contribuire al repo Bun se possibile, e aver pronto un piano di rollback a Node.js.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti