Zum Inhalt springen
·21 Min. Lesezeit

Bun in Produktion: Was funktioniert, was nicht und was mich überrascht hat

Bun als Runtime, Package Manager, Bundler und Test Runner. Echte Benchmarks, Node.js-Kompatibilitätslücken, Migrationsmuster und wo ich Bun heute in Produktion einsetze.

Teilen:X / TwitterLinkedIn

Alle paar Jahre bekommt das JavaScript-Ökosystem eine neue Runtime und der Diskurs folgt einem vorhersehbaren Bogen. Hype. Benchmarks. "X ist tot." Realitätscheck. Einpendeln auf die tatsächlichen Anwendungsfälle, bei denen das neue Tool wirklich glänzt.

Bun ist gerade mitten in diesem Bogen. Und im Gegensatz zu den meisten Herausforderern bleibt es bestehen. Nicht weil es "schneller" ist (obwohl es das oft ist), sondern weil es ein grundlegend anderes Problem löst: Die JavaScript-Toolchain hat zu viele bewegliche Teile, und Bun fasst sie in eins zusammen.

Ich benutze Bun seit über einem Jahr in verschiedenen Kapazitäten. Einiges davon in Produktion. Einiges davon als Ersatz für Tools, von denen ich dachte, ich würde sie nie ersetzen. Dieser Beitrag ist eine ehrliche Bestandsaufnahme von dem, was funktioniert, was nicht und wo die Lücken noch eine Rolle spielen.

Was Bun eigentlich ist#

Das erste Missverständnis, das es aufzuklären gilt: Bun ist nicht "ein schnelleres Node.js." Diese Einordnung wird dem nicht gerecht.

Bun ist vier Tools in einem Binary:

  1. Eine JavaScript/TypeScript-Runtime — führt deinen Code aus, wie Node.js oder Deno
  2. Ein Package Manager — ersetzt npm, yarn oder pnpm
  3. Ein Bundler — ersetzt esbuild, webpack oder Rollup für bestimmte Anwendungsfälle
  4. Ein Test Runner — ersetzt Jest oder Vitest für die meisten Testsuiten

Das Schlüsselwort ist "eins." Du installierst ein Binary und bekommst Fähigkeiten, die normalerweise 4-5 separate Tools, jeweils mit eigener Konfiguration, erfordern.

Unter der Haube nutzt Bun JavaScriptCore (die Engine hinter Safari), nicht V8 (die Engine hinter Chrome und Node.js). Das ist wichtig, weil es erklärt, warum manche Performance-Charakteristiken sich von Node.js unterscheiden — es sind verschiedene JIT-Compiler mit verschiedenen Optimierungsstrategien.

Die tatsächlichen Benchmarks#

Lass uns über Zahlen reden. Nicht Marketing-Zahlen — tatsächliche Messungen, die ich auf meinem Rechner durchgeführt habe.

Startzeit#

bash
# Startzeit messen — jeweils 100 Mal
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Typische Ergebnisse:
# node:  ~40ms
# bun:   ~6ms

Das ist ungefähr ein 6-7-facher Unterschied bei der Startzeit. Für Skripte, CLI-Tools und Serverless Functions, bei denen Cold Start eine Rolle spielt, ist das signifikant. Für einen lang laufenden Serverprozess, der einmal startet und wochenlang läuft, ist es irrelevant.

Paketinstallation#

Das ist der andere Bereich, in dem Bun die Konkurrenz blamiert.

bash
# Clean-Install-Benchmark — node_modules und Lockfile vorher löschen
rm -rf node_modules bun.lockb package-lock.json
 
# npm timen
time npm install
# Real: ~18.4s (typisches mittelgroßes Projekt)
 
# bun timen
time bun install
# Real: ~2.1s

Das ist ein 8-9-facher Unterschied, und er ist konsistent. Die Gründe sind primär:

  1. Binärer Lockfilebun.lockb ist ein Binärformat, kein JSON. Schneller zu lesen und zu schreiben.
  2. Globaler Cache — Bun pflegt einen globalen Modul-Cache, sodass Reinstallationen über Projekte hinweg heruntergeladene Pakete teilen.
  3. Zigs I/O — Der Package Manager selbst ist in Zig geschrieben, nicht in JavaScript. Datei-I/O-Operationen sind näher am Metal.
  4. Symlink-Strategie — Bun nutzt Hardlinks vom globalen Cache statt Dateien zu kopieren.

HTTP-Server-Durchsatz#

Buns eingebauter HTTP-Server ist schnell, aber die Vergleiche brauchen Kontext.

bash
# Schneller und unsauberer Benchmark mit bombardier
# Eine einfache "Hello World"-Antwort testen
 
# Bun Server
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
 
# Node.js (natives http-Modul)
bombardier -c 100 -d 10s http://localhost:3001
# Requests/sec: ~48,000
 
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# Requests/sec: ~15,000

Bun vs. rohes Node.js: ungefähr 2x für triviale Antworten. Bun vs. Express: ungefähr 7x, aber das ist unfair, weil Express Middleware-Overhead hinzufügt. Sobald du echte Logik hinzufügst — Datenbankabfragen, Authentifizierung, JSON-Serialisierung echter Daten — schrumpft der Abstand dramatisch.

Wo der Unterschied vernachlässigbar ist#

CPU-gebundene Berechnungen:

typescript
// fibonacci.ts — das ist Engine-gebunden, nicht Runtime-gebunden
function fib(n: number): number {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
 
const start = performance.now();
console.log(fib(42));
console.log(`${(performance.now() - start).toFixed(0)}ms`);
bash
bun run fibonacci.ts   # ~1650ms
node fibonacci.ts      # ~1580ms

Node.js (V8) gewinnt hier sogar leicht. V8s JIT-Compiler ist aggressiver bei heißen Schleifen. Für CPU-gebundene Arbeit sind die Engine-Unterschiede ein Unentschieden — manchmal gewinnt V8, manchmal gewinnt JSC, und die Unterschiede liegen im Rauschen.

Eigene Benchmarks durchführen#

Vertraue niemandens Benchmarks, einschließlich meiner. So misst du, was für deine spezifische Arbeitslast zählt:

bash
# hyperfine für ordentliches Benchmarking installieren
brew install hyperfine  # macOS
# oder: cargo install hyperfine
 
# Startzeit + Ausführung deiner tatsächlichen App benchmarken
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# Für HTTP-Server bombardier oder wrk verwenden
# Wichtig: mit realistischen Payloads testen, nicht "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# Speichervergleich
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

Die Faustregel: Wenn dein Bottleneck I/O ist (Dateisystem, Netzwerk, Datenbank), ist Buns Vorteil bescheiden. Wenn dein Bottleneck Startzeit oder Toolchain-Geschwindigkeit ist, gewinnt Bun groß. Wenn dein Bottleneck rohe Berechnung ist, ist es ein Unentschieden.

Bun als Package Manager#

Hier habe ich komplett gewechselt. Selbst bei Projekten, in denen ich Node.js in Produktion laufen lasse, benutze ich bun install für die lokale Entwicklung und CI. Es ist einfach schneller, und die Kompatibilität ist ausgezeichnet.

Die Grundlagen#

bash
# Alle Dependencies aus package.json installieren
bun install
 
# Dependency hinzufügen
bun add express
 
# Dev-Dependency hinzufügen
bun add -d vitest
 
# Dependency entfernen
bun remove express
 
# Dependency aktualisieren
bun update express
 
# Bestimmte Version installieren
bun add express@4.18.2

Wenn du npm oder yarn benutzt hast, ist das komplett vertraut. Die Flags sind leicht anders (-d statt --save-dev), aber das mentale Modell ist identisch.

Die Lockfile-Situation#

Bun nutzt bun.lockb, einen binären Lockfile. Das ist sowohl seine Superpower als auch sein größter Reibungspunkt.

Das Gute: Er ist dramatisch schneller zu lesen und zu schreiben. Das Binärformat bedeutet, dass Bun den Lockfile in Mikrosekunden parsen kann, nicht die Hunderten von Millisekunden, die npm für das Parsen von package-lock.json aufwendet.

Das Schlechte: Du kannst ihn nicht in einem Diff reviewen. Wenn du in einem Team bist und jemand eine Dependency aktualisiert, kannst du dir den Lockfile-Diff in einem PR nicht anschauen und sehen, was sich geändert hat. Das ist wichtiger als Geschwindigkeitsbefürworter zugeben wollen.

bash
# Lockfile in menschenlesbares Format dumpen
bun bun.lockb > lockfile-dump.txt
 
# Oder die eingebaute Textausgabe verwenden
bun install --yarn
# Das generiert einen yarn.lock neben bun.lockb

Mein Ansatz: Ich committe bun.lockb ins Repo und generiere auch einen yarn.lock oder package-lock.json als lesbaren Fallback. Gürtel und Hosenträger.

Workspace-Support#

Bun unterstützt npm/yarn-Style Workspaces:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Dependencies für alle Workspaces installieren
bun install
 
# Script in einem bestimmten Workspace ausführen
bun run --filter packages/shared build
 
# Dependency zu einem bestimmten Workspace hinzufügen
bun add react --filter apps/web

Workspace-Support ist solide und hat sich deutlich verbessert. Die Hauptlücke im Vergleich zu pnpm ist, dass Buns Workspace-Dependency-Auflösung weniger strikt ist — pnpms Striktheit ist ein Feature für Monorepos, weil sie Phantom-Dependencies abfängt.

Kompatibilität mit bestehenden Projekten#

Du kannst bun install in fast jedes bestehende Node.js-Projekt einsetzen. Es liest package.json, respektiert .npmrc für Registry-Konfiguration und handhabt peerDependencies korrekt. Der Übergang ist typischerweise:

bash
# Schritt 1: Bestehenden Lockfile und node_modules löschen
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# Schritt 2: Mit Bun installieren
bun install
 
# Schritt 3: Verifizieren, dass deine App noch funktioniert
bun run dev
# oder: node dist/server.js  (Bun Package Manager, Node Runtime)

Ich habe das bei einem Dutzend Projekten gemacht und hatte null Probleme mit dem Package Manager selbst. Der einzige Haken ist, wenn deine CI-Pipeline speziell nach package-lock.json sucht — du musst sie aktualisieren, um bun.lockb zu handhaben.

Node.js-Kompatibilität#

Das ist der Abschnitt, in dem ich am vorsichtigsten sein muss, weil sich die Situation jeden Monat ändert. Stand Anfang 2026 hier das ehrliche Bild.

Was funktioniert#

Die überwiegende Mehrheit der npm-Pakete funktioniert ohne Modifikation. Bun implementiert die meisten eingebauten Node.js-Module:

typescript
// Diese funktionieren alle wie erwartet in Bun
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { EventEmitter } from "node:events";
import { Readable, Writable } from "node:stream";
import http from "node:http";
import https from "node:https";
import { URL, URLSearchParams } from "node:url";
import os from "node:os";
import child_process from "node:child_process";

Sowohl CommonJS als auch ESM funktionieren. require() und import können koexistieren. TypeScript läuft ohne Kompilierungsschritt — Bun entfernt Types beim Parsen.

Frameworks die funktionieren:

  • Express — funktioniert, einschließlich Middleware-Ökosystem
  • Fastify — funktioniert
  • Hono — funktioniert (und ist ausgezeichnet mit Bun)
  • Next.js — funktioniert mit Vorbehalten (dazu später mehr)
  • Prisma — funktioniert
  • Drizzle ORM — funktioniert
  • Socket.io — funktioniert

Was nicht funktioniert (oder Probleme hat)#

Die Lücken fallen tendenziell in einige Kategorien:

Native Addons (node-gyp): Wenn ein Paket C++-Addons verwendet, die mit node-gyp kompiliert werden, funktioniert es möglicherweise nicht mit Bun. Bun hat ein eigenes FFI-System und unterstützt viele native Module, aber die Abdeckung ist nicht 100%. Zum Beispiel hatte bcrypt (das native) Probleme — verwende stattdessen bcryptjs.

bash
# Prüfen ob ein Paket native Addons verwendet
ls node_modules/your-package/binding.gyp  # Wenn das existiert, ist es nativ

Bestimmte Node.js-Internals: Manche Pakete greifen auf Node.js-Internals wie process.binding() zu oder nutzen V8-spezifische APIs. Diese funktionieren nicht in Bun, da es auf JavaScriptCore läuft.

typescript
// Das funktioniert NICHT in Bun — V8-spezifisch
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// Das FUNKTIONIERT — verwende Buns Äquivalent oder einen Cross-Runtime-Ansatz
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker Threads: Bun unterstützt Web Workers und node:worker_threads, aber es gibt Randfälle. Manche fortgeschrittene Nutzungsmuster — besonders um SharedArrayBuffer und Atomics — können sich unterschiedlich verhalten.

vm-Modul: node:vm hat teilweise Unterstützung. Wenn dein Code oder eine Dependency vm.createContext() extensiv nutzt (manche Template-Engines tun das), teste gründlich.

Der Kompatibilitäts-Tracker#

Bun pflegt einen offiziellen Kompatibilitäts-Tracker. Prüfe ihn, bevor du dich für ein Projekt auf Bun festlegst:

bash
# Buns eingebauten Kompatibilitätscheck auf deinem Projekt ausführen
bun --bun node_modules/.bin/your-tool
 
# Das --bun Flag erzwingt Buns Runtime auch für node_modules Scripts

Meine Empfehlung: Nimm Kompatibilität nicht an. Lasse deine Testsuite unter Bun laufen, bevor du dich entscheidest. Es dauert fünf Minuten und spart Stunden Debugging.

bash
# Schneller Kompatibilitätscheck — deine vollständige Testsuite unter Bun laufen lassen
bun test  # Wenn du bun test verwendest
# oder
bun run vitest  # Wenn du vitest verwendest

Buns eingebaute APIs#

Hier wird Bun interessant. Statt nur Node.js-APIs neu zu implementieren, bietet Bun eigene APIs, die einfacher und schneller konzipiert sind.

Bun.serve() — Der eingebaute HTTP-Server#

Das ist die API, die ich am meisten nutze. Sie ist sauber, schnell, und WebSocket-Support ist direkt eingebaut.

typescript
const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
 
    if (url.pathname === "/") {
      return new Response("Hello from Bun!", {
        headers: { "Content-Type": "text/plain" },
      });
    }
 
    if (url.pathname === "/api/users") {
      const users = [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ];
      return Response.json(users);
    }
 
    return new Response("Not Found", { status: 404 });
  },
});
 
console.log(`Server running at http://localhost:${server.port}`);

Ein paar Dinge zu beachten:

  1. Web-Standard Request/Response — keine proprietäre API. Der fetch-Handler empfängt einen Standard-Request und gibt einen Standard-Response zurück. Wenn du einen Cloudflare Worker geschrieben hast, fühlt sich das identisch an.
  2. Response.json() — eingebauter JSON-Response-Helper.
  3. Kein Import nötigBun.serve ist global. Kein require("http").

Hier ein realistischeres Beispiel mit Routing, JSON-Body-Parsing und Error Handling:

typescript
import { Database } from "bun:sqlite";
 
const db = new Database("app.db");
db.run(`
  CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    completed INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
const server = Bun.serve({
  port: process.env.PORT || 3000,
 
  async fetch(req) {
    const url = new URL(req.url);
    const method = req.method;
 
    try {
      // GET /api/todos
      if (url.pathname === "/api/todos" && method === "GET") {
        const todos = db.query("SELECT * FROM todos ORDER BY created_at DESC").all();
        return Response.json(todos);
      }
 
      // POST /api/todos
      if (url.pathname === "/api/todos" && method === "POST") {
        const body = await req.json();
 
        if (!body.title || typeof body.title !== "string") {
          return Response.json({ error: "Title is required" }, { status: 400 });
        }
 
        const stmt = db.prepare("INSERT INTO todos (title) VALUES (?) RETURNING *");
        const todo = stmt.get(body.title);
        return Response.json(todo, { status: 201 });
      }
 
      // DELETE /api/todos/:id
      const deleteMatch = url.pathname.match(/^\/api\/todos\/(\d+)$/);
      if (deleteMatch && method === "DELETE") {
        const id = parseInt(deleteMatch[1], 10);
        db.run("DELETE FROM todos WHERE id = ?", [id]);
        return new Response(null, { status: 204 });
      }
 
      return Response.json({ error: "Not found" }, { status: 404 });
    } catch (error) {
      console.error("Request error:", error);
      return Response.json({ error: "Internal server error" }, { status: 500 });
    }
  },
});
 
console.log(`Server running on port ${server.port}`);

Das ist ein vollständiges CRUD-API mit SQLite in etwa 50 Zeilen. Kein Express, kein ORM, keine Middleware-Chain. Für kleine APIs und interne Tools ist das mittlerweile mein Go-to-Setup.

Bun.file() und Bun.write() — Datei-I/O#

Buns Datei-API ist erfrischend einfach im Vergleich zu fs.readFile():

typescript
// Dateien lesen
const file = Bun.file("./config.json");
const text = await file.text();       // Als String lesen
const json = await file.json();       // Direkt als JSON parsen
const bytes = await file.arrayBuffer(); // Als ArrayBuffer lesen
const stream = file.stream();          // Als ReadableStream lesen
 
// Dateimetadaten
console.log(file.size);  // Größe in Bytes
console.log(file.type);  // MIME-Typ (z.B. "application/json")
 
// Dateien schreiben
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.json", JSON.stringify({ key: "value" }));
await Bun.write("./copy.png", Bun.file("./original.png"));
 
// Response-Body in eine Datei schreiben
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

Die Bun.file()-API ist lazy — sie liest die Datei nicht, bis du .text(), .json() usw. aufrufst. Das bedeutet, du kannst Bun.file()-Referenzen herumreichen, ohne I/O-Kosten zu verursachen, bis du die Daten tatsächlich brauchst.

Eingebauter WebSocket-Support#

WebSockets sind erstklassig in Bun.serve():

typescript
const server = Bun.serve({
  port: 3000,
 
  fetch(req, server) {
    const url = new URL(req.url);
 
    if (url.pathname === "/ws") {
      const upgraded = server.upgrade(req, {
        data: {
          userId: url.searchParams.get("userId"),
          joinedAt: Date.now(),
        },
      });
 
      if (!upgraded) {
        return new Response("WebSocket upgrade failed", { status: 400 });
      }
      return undefined;
    }
 
    return new Response("Use /ws for WebSocket connections");
  },
 
  websocket: {
    open(ws) {
      console.log(`Client connected: ${ws.data.userId}`);
      ws.subscribe("chat");
    },
 
    message(ws, message) {
      // An alle Subscriber broadcasten
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

Das server.publish()- und ws.subscribe()-Pattern ist eingebautes Pub/Sub. Kein Redis, keine separate WebSocket-Bibliothek. Für einfache Echtzeit-Features ist das unglaublich praktisch.

Eingebautes SQLite mit bun:sqlite#

Das hat mich am meisten überrascht. Bun liefert SQLite direkt in der Runtime mit:

typescript
import { Database } from "bun:sqlite";
 
// Datenbank öffnen oder erstellen
const db = new Database("myapp.db");
 
// WAL-Modus für bessere parallele Leseleistung
db.exec("PRAGMA journal_mode = WAL");
 
// Tabellen erstellen
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);
 
// Prepared Statements (wiederverwendbar, schneller für wiederholte Abfragen)
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
 
const findByEmail = db.prepare(
  "SELECT * FROM users WHERE email = $email"
);
 
// Verwendung
const user = insertUser.get({
  $email: "alice@example.com",
  $name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
 
// Transaktionen
const insertMany = db.transaction((users: { email: string; name: string }[]) => {
  for (const user of users) {
    insertUser.run({ $email: user.email, $name: user.name });
  }
  return users.length;
});
 
const count = insertMany([
  { email: "bob@example.com", name: "Bob" },
  { email: "carol@example.com", name: "Carol" },
]);
console.log(`Inserted ${count} users`);

Das ist synchrones SQLite mit der Performance einer C-Bibliothek (denn es ist eine — Bun bettet libsqlite3 direkt ein). Für CLI-Tools, Local-First-Apps und kleine Services bedeutet eingebautes SQLite null externe Abhängigkeiten für deine Datenschicht.

Bun Test Runner#

bun test ist ein Drop-in-Ersatz für Jest in den meisten Fällen. Es nutzt dasselbe describe/it/expect-API und unterstützt die meisten Jest-Matcher.

Grundlegende Nutzung#

typescript
// math.test.ts
import { describe, it, expect } from "bun:test";
 
describe("math utilities", () => {
  it("adds numbers correctly", () => {
    expect(1 + 2).toBe(3);
  });
 
  it("handles floating point", () => {
    expect(0.1 + 0.2).toBeCloseTo(0.3);
  });
});
bash
# Alle Tests ausführen
bun test
 
# Bestimmte Datei ausführen
bun test math.test.ts
 
# Tests mit Muster ausführen
bun test --test-name-pattern "adds numbers"
 
# Watch-Modus
bun test --watch
 
# Coverage
bun test --coverage

Mocking#

Bun unterstützt Jest-kompatibles Mocking:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Modul mocken
mock.module("./database", () => ({
  query: mock(() => [{ id: 1, name: "Alice" }]),
}));
 
describe("fetchUsers", () => {
  it("returns users from database", async () => {
    const users = await fetchUsers();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe("Alice");
  });
});
 
// Methode auf einem Objekt ausspionieren
describe("console", () => {
  it("tracks console.log calls", () => {
    const logSpy = spyOn(console, "log");
    console.log("test message");
    expect(logSpy).toHaveBeenCalledWith("test message");
    logSpy.mockRestore();
  });
});

Bun Test vs. Vitest — Mein ehrlicher Vergleich#

Ich verwende Vitest für dieses Projekt (und die meisten meiner Projekte). Hier ist, warum ich nicht komplett gewechselt habe:

Wo bun test gewinnt:

  • Startgeschwindigkeit. bun test beginnt Tests auszuführen, schneller als Vitest seine Config fertig laden kann.
  • Null Konfiguration. Kein vitest.config.ts nötig für einfache Setups.
  • Eingebautes TypeScript. Kein Transformationsschritt.

Wo Vitest weiterhin gewinnt:

  • Ökosystem: Vitest hat mehr Plugins, bessere IDE-Integration und eine größere Community.
  • Konfiguration: Vitests Config-System ist flexibler. Eigene Reporter, komplexe Setup-Dateien, mehrere Test-Umgebungen.
  • Browser-Modus: Vitest kann Tests in einem echten Browser ausführen. Bun kann das nicht.
  • Kompatibilität: Manche Test-Bibliotheken (Testing Library, MSW) wurden gründlicher mit Vitest/Jest getestet.
  • Snapshot-Testing: Beide unterstützen es, aber Vitests Implementierung ist ausgereifter mit besserer Diff-Ausgabe.

Für ein neues Projekt mit einfachen Testanforderungen würde ich bun test verwenden. Für ein etabliertes Projekt mit Testing Library, MSW und komplexem Mocking behalte ich Vitest.

Bun Bundler#

bun build ist ein schneller JavaScript/TypeScript-Bundler. Er ist kein webpack-Ersatz — er ist eher in der esbuild-Kategorie: schnell, meinungsstark und auf die gängigen Fälle fokussiert.

Grundlegendes Bundling#

bash
# Einzelnen Entry Point bundeln
bun build ./src/index.ts --outdir ./dist
 
# Für verschiedene Targets bundeln
bun build ./src/index.ts --outdir ./dist --target browser
bun build ./src/index.ts --outdir ./dist --target bun
bun build ./src/index.ts --outdir ./dist --target node
 
# Minifizieren
bun build ./src/index.ts --outdir ./dist --minify
 
# Sourcemaps generieren
bun build ./src/index.ts --outdir ./dist --sourcemap external

Programmatisches API#

typescript
const result = await Bun.build({
  entrypoints: ["./src/index.ts", "./src/worker.ts"],
  outdir: "./dist",
  target: "browser",
  minify: {
    whitespace: true,
    identifiers: true,
    syntax: true,
  },
  splitting: true,    // Code Splitting
  sourcemap: "external",
  external: ["react", "react-dom"],  // Diese nicht bundeln
  naming: "[dir]/[name]-[hash].[ext]",
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
});
 
if (!result.success) {
  console.error("Build failed:");
  for (const log of result.logs) {
    console.error(log);
  }
  process.exit(1);
}
 
for (const output of result.outputs) {
  console.log(`${output.path} — ${output.size} bytes`);
}

Wo Bun Build Grenzen hat#

  • Kein CSS-Bundling — Du brauchst ein separates Tool für CSS (PostCSS, Lightning CSS, Tailwind CLI).
  • Keine HTML-Generierung — Es bundelt JavaScript/TypeScript, keine vollständigen Web-Apps.
  • Plugin-Ökosystem — esbuild hat ein viel größeres Plugin-Ökosystem. Buns Plugin-API ist kompatibel, aber die Community ist kleiner.
  • Fortgeschrittenes Code Splitting — Webpack und Rollup bieten immer noch ausgefeiltere Chunk-Strategien.

Für das Bauen einer Bibliothek oder eines einfachen JS-Bundles einer Web-App ist bun build ausgezeichnet. Für komplexe App-Builds mit CSS Modules, Bildoptimierung und eigenen Chunk-Strategien brauchst du weiterhin einen vollständigen Bundler.

Bun Macros#

Ein wirklich einzigartiges Feature: Compile-Time Code-Ausführung über Macros.

typescript
// build-info.ts — diese Datei läuft zur BUILD-ZEIT, nicht zur Laufzeit
export function getBuildInfo() {
  return {
    builtAt: new Date().toISOString(),
    gitSha: require("child_process")
      .execSync("git rev-parse --short HEAD")
      .toString()
      .trim(),
    nodeVersion: process.version,
  };
}
typescript
// app.ts
import { getBuildInfo } from "./build-info" with { type: "macro" };
 
// getBuildInfo() wird zur Bundle-Zeit ausgeführt
// Das Ergebnis wird als statischer Wert inlined
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

Nach dem Bundling wird getBuildInfo() durch das literale Objekt ersetzt — kein Funktionsaufruf zur Laufzeit, kein Import von child_process. Der Code lief während des Builds und das Ergebnis wurde inlined. Das ist mächtig für das Einbetten von Build-Metadaten, Feature Flags oder umgebungsspezifischer Konfiguration.

Bun mit Next.js verwenden#

Das ist die Frage, die mir am häufigsten gestellt wird, also lass mich sehr spezifisch sein.

Was heute funktioniert#

Bun als Package Manager für Next.js — funktioniert perfekt:

bash
# Bun verwenden, um Dependencies zu installieren, dann Node.js um Next.js auszuführen
bun install
bun run dev    # Das führt tatsächlich das "dev"-Script standardmäßig über Node.js aus
bun run build
bun run start

Das ist, was ich für jedes Next.js-Projekt mache. Der bun run <script>-Befehl liest den scripts-Abschnitt der package.json und führt ihn aus. Standardmäßig nutzt er das System-Node.js für die tatsächliche Ausführung. Du bekommst Buns schnelle Paketinstallation, ohne deine Runtime zu ändern.

Bun Runtime für Next.js Development:

bash
# Next.js unter Buns Runtime erzwingen
bun --bun run dev

Das funktioniert für die Entwicklung in den meisten Fällen. Das --bun-Flag sagt Bun, seine eigene Runtime zu verwenden statt an Node.js zu delegieren. Hot Module Replacement funktioniert. API-Routes funktionieren. Server Components funktionieren.

Was noch experimentell ist#

Bun Runtime für Next.js Production Builds:

bash
# Mit Bun Runtime bauen
bun --bun run build
 
# Produktionsserver mit Bun Runtime starten
bun --bun run start

Das funktioniert für viele Projekte, aber ich bin auf Randfälle gestoßen:

  1. Manche Middleware-Verhaltensweisen unterscheiden sich — Wenn du Next.js Middleware nutzt, die von Node.js-spezifischen APIs abhängt, könntest du Kompatibilitätsprobleme haben.
  2. Bildoptimierung — Next.js' Bildoptimierungspipeline nutzt sharp, ein natives Addon. Es funktioniert mit Bun, aber ich habe gelegentliche Probleme gesehen.
  3. ISR (Incremental Static Regeneration) — Funktioniert, aber ich habe es nicht unter Last in Produktion mit Bun getestet.

Meine Empfehlung für Next.js#

Nutze Bun als Package Manager. Nutze Node.js als Runtime. Das gibt dir die Geschwindigkeitsvorteile von bun install ohne jedes Kompatibilitätsrisiko.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Täglicher Workflow
bun install      # Schnelle Paketinstallation
bun run dev      # Führt "next dev" via Node.js aus
bun run build    # Führt "next build" via Node.js aus

Wenn Buns Node.js-Kompatibilität 100% für Next.js' interne Nutzung erreicht (es ist nah dran, aber noch nicht da), werde ich wechseln. Bis dahin spart mir allein der Package Manager genug Zeit, um die Installation zu rechtfertigen.

Docker mit Bun#

Das offizielle Bun Docker Image ist gut gepflegt und produktionsreif.

Einfaches Dockerfile#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# Dependencies installieren
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# Build (falls nötig)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
 
# Produktion
FROM base AS production
WORKDIR /app
 
# Nicht als root laufen
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
 
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]

Kompilierung in ein einzelnes Binary#

Das ist eines von Buns Killer-Features für Deployments:

bash
# Deine App in ein eigenständiges Executable kompilieren
bun build --compile ./src/server.ts --outfile server
 
# Der Output ist ein eigenständiges Binary — kein Bun oder Node.js zum Ausführen nötig
./server
dockerfile
# Ultra-minimales Docker Image mit kompiliertem Binary
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --compile ./src/server.ts --outfile server
 
# Finales Image — nur das Binary
FROM debian:bookworm-slim
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/server ./server
 
EXPOSE 3000
CMD ["./server"]

Das kompilierte Binary ist typischerweise 50-90 MB (es bundelt die Bun-Runtime). Das ist größer als ein Go-Binary, aber viel kleiner als eine volle Node.js-Installation plus node_modules. Für containerisierte Deployments ist die eigenständige Natur eine deutliche Vereinfachung.

Größenvergleich#

bash
# Node.js Image
docker images | grep node
# node:20-slim    ~180MB
 
# Bun Image
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# Kompiliertes Binary auf debian:bookworm-slim
# ~80MB Basis + ~70MB Binary = ~150MB gesamt
 
# vs. Alpine mit Node.js
# node:20-alpine   ~130MB + node_modules

Der Binary-Ansatz eliminiert node_modules komplett aus dem finalen Image. Kein npm install in Produktion. Keine Supply-Chain-Angriffsfläche von Hunderten von Paketen. Nur eine Datei.

Migrationsmuster#

Wenn du überlegst, zu Bun zu wechseln, hier der inkrementelle Pfad, den ich empfehle:

Phase 1: Nur Package Manager (Null Risiko)#

bash
# npm/yarn/pnpm durch bun install ersetzen
# Deine CI-Pipeline ändern:
# Vorher:
npm ci
 
# Nachher:
bun install --frozen-lockfile

Keine Code-Änderungen. Keine Runtime-Änderungen. Nur schnellere Installationen. Wenn irgendetwas kaputtgeht (wird es nicht), revertiere, indem du bun.lockb löschst und npm install ausführst.

Phase 2: Scripts und Tooling#

bash
# Bun für Entwicklungsscripts verwenden
bun run dev
bun run lint
bun run format
 
# Bun für einmalige Scripts verwenden
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

Node.js wird immer noch als Runtime für deine tatsächliche Anwendung verwendet. Aber Scripts profitieren von Buns schnellerem Start und nativer TypeScript-Unterstützung.

Phase 3: Test Runner (Mittleres Risiko)#

bash
# vitest/jest durch bun test für einfache Testsuiten ersetzen
bun test
 
# Vitest für komplexe Test-Setups behalten
# (Testing Library, MSW, benutzerdefinierte Umgebungen)

Lasse deine vollständige Testsuite unter bun test laufen. Wenn alles besteht, hast du eine devDependency eliminiert. Wenn manche Tests wegen Kompatibilität fehlschlagen, behalte Vitest für diese und nutze bun test für den Rest.

Phase 4: Runtime für neue Services (Kalkuliertes Risiko)#

typescript
// Neue Microservices oder APIs — von Anfang an mit Bun starten
Bun.serve({
  port: 3000,
  fetch(req) {
    // Dein neuer Service hier
  },
});

Migriere bestehende Node.js-Services nicht auf die Bun-Runtime. Schreibe stattdessen neue Services von Anfang an mit Bun. Das begrenzt deinen Blast Radius.

Phase 5: Runtime-Migration (Fortgeschritten)#

bash
# Nur nach gründlichem Testen:
# Node durch Bun für bestehende Services ersetzen
# Vorher:
node dist/server.js
 
# Nachher:
bun dist/server.js

Ich empfehle das nur für Services mit exzellenter Testabdeckung. Lasse deine Lasttests unter Bun laufen, bevor du die Produktion umstellst.

Umgebungsvariablen und Konfiguration#

Bun handhabt .env-Dateien automatisch — kein dotenv-Paket nötig:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// Diese sind ohne Import verfügbar
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun-spezifische Alternative

Bun lädt .env, .env.local, .env.production usw. automatisch und folgt derselben Konvention wie Next.js. Eine Dependency weniger in deiner package.json.

Häufige Stolperfallen#

Nach einem Jahr Nutzung von Bun, hier die Dinge, die mich ins Stolpern gebracht haben:

1. Unterschiedliches globales Fetch-Verhalten#

typescript
// Node.js 18+ fetch und Buns fetch unterscheiden sich leicht
// in der Handhabung bestimmter Header und Redirects
 
// Bun folgt Redirects standardmäßig (wie Browser)
// Node.js fetch folgt auch Redirects, aber das Verhalten
// bei bestimmten Statuscodes (303, 307, 308) kann sich unterscheiden
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // Explizit die Redirect-Handhabung angeben
});

2. Process-Exit-Verhalten#

typescript
// Bun beendet sich, wenn die Event Loop leer ist
// Node.js läuft manchmal weiter wegen verbleibender Handles
 
// Wenn dein Bun-Script unerwartet beendet wird, hält etwas
// die Event Loop nicht am Leben
 
// Das beendet sich sofort in Bun:
setTimeout(() => {}, 0);
 
// Das läuft weiter:
setTimeout(() => {}, 1000);
// (Bun beendet sich, nachdem der Timeout feuert)

3. TypeScript-Konfiguration#

typescript
// Bun hat eigene tsconfig-Defaults
// Wenn du ein Projekt zwischen Bun und Node.js teilst,
// sei explizit in deiner tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Bun-Typdefinitionen hinzufügen
  }
}
bash
# Bun Types installieren
bun add -d @types/bun

4. Hot Reload in der Entwicklung#

bash
# Bun hat eingebauten Watch-Modus
bun --watch run server.ts
 
# Das startet den Prozess bei Dateiänderungen neu
# Es ist kein HMR (Hot Module Replacement) — es ist ein voller Neustart
# Aber weil Bun so schnell startet, fühlt es sich instant an

5. Die bunfig.toml-Konfigurationsdatei#

toml
# bunfig.toml — Buns Config-Datei (optional)
 
[install]
# Private Registry verwenden
registry = "https://npm.mycompany.com"
 
# Scoped Registries
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# Test-Konfiguration
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# Shell für bun run
shell = "bash"

Mein Fazit#

Nach einem Jahr Produktionsnutzung, hier wo ich mich eingependelt habe:

Wo ich Bun heute einsetze#

Package Manager für alle Projekte — einschließlich dieses Next.js-Blogs. bun install ist schneller, und die Kompatibilität ist im Wesentlichen perfekt. Ich sehe keinen Grund mehr, npm oder yarn zu verwenden. pnpm ist die einzige Alternative, die ich in Betracht ziehen würde (für seine strikte Dependency-Auflösung in Monorepos).

Runtime für Scripts und CLI-Tools — Jede TypeScript-Datei, die ich einmal ausführen muss, lasse ich mit bun laufen. Kein Kompilierungsschritt. Schneller Start. Eingebautes .env-Laden. Es hat ts-node und tsx in meinem Workflow komplett ersetzt.

Runtime für kleine APIs und interne ToolsBun.serve() + bun:sqlite ist ein unglaublich produktiver Stack für interne Tools, Webhook-Handler und kleine Services. Das "ein Binary, keine Dependencies"-Deployment-Modell ist überzeugend.

Test Runner für einfache Projekte — Für Projekte mit unkomplizierten Testanforderungen ist bun test schnell und braucht null Konfiguration.

Wo ich bei Node.js bleibe#

Produktions-Next.js — Nicht weil Bun nicht funktioniert, sondern weil das Risiko-Ertrags-Verhältnis es noch nicht rechtfertigt. Next.js ist ein komplexes Framework mit vielen Integrationspunkten. Ich will die am meisten kampferprobte Runtime darunter.

Kritische Produktions-Services — Meine Haupt-API-Server laufen Node.js hinter PM2. Das Monitoring-Ökosystem, die Debugging-Tools, das operationelle Wissen — das ist alles Node.js. Bun wird dort hinkommen, aber es ist noch nicht so weit.

Alles mit nativen Addons — Wenn eine Dependency-Chain C++ native Addons enthält, versuche ich Bun gar nicht erst. Nicht wert, die Kompatibilitätsprobleme zu debuggen.

Teams, die nicht mit Bun vertraut sind — Bun als Runtime in ein Team einzuführen, das es nie benutzt hat, fügt kognitive Last hinzu. Als Package Manager, in Ordnung. Als Runtime, warte bis das Team bereit ist.

Was ich beobachte#

Buns Kompatibilitäts-Tracker — Wenn er 100% für die Node.js-APIs erreicht, die mich interessieren, werde ich neu bewerten.

Framework-Support — Next.js, Remix und SvelteKit haben alle unterschiedliche Level an Bun-Support. Wenn eines davon Bun offiziell als Produktions-Runtime unterstützt, ist das ein Signal.

Enterprise-Adoption — Sobald Firmen mit echten SLAs Bun in Produktion laufen lassen und darüber schreiben, ist die Reifefrage beantwortet.

Die 1.2+ Release-Linie — Bun bewegt sich schnell. Features landen jede Woche. Das Bun, das ich heute nutze, ist spürbar besser als das Bun, das ich vor einem Jahr ausprobiert habe.

Abschluss#

Bun ist keine Wunderwaffe. Es macht eine langsame App nicht schnell und ein schlecht designtes API nicht gut designed. Aber es ist eine echte Verbesserung der Entwicklererfahrung für das JavaScript-Ökosystem.

Was ich an Bun am meisten schätze, ist kein einzelnes Feature. Es ist die Reduktion der Toolchain-Komplexität. Ein Binary, das Pakete installiert, TypeScript ausführt, Code bundelt und Tests laufen lässt. Keine tsconfig.json für Scripts. Kein Babel. Keine separate Test-Runner-Config. Einfach bun run your-file.ts und es funktioniert.

Der praktische Rat: Starte mit bun install. Es ist null Risiko, sofortiger Nutzen. Dann probiere bun run für Scripts. Dann evaluiere den Rest basierend auf deinen spezifischen Anforderungen. Du musst nicht All-in gehen. Bun funktioniert perfekt als partieller Ersatz, und das ist wahrscheinlich, wie die meisten Leute es heute nutzen sollten.

Die JavaScript-Runtime-Landschaft ist besser mit Bun darin. Wettbewerb macht auch Node.js besser — Node.js 22+ ist deutlich schneller geworden, teilweise als Reaktion auf Buns Druck. Alle gewinnen.

Ähnliche Beiträge