Ga naar inhoud
·25 min leestijd

Bun in Productie: Wat Werkt, Wat Niet, en Wat Me Verraste

Bun als runtime, package manager, bundler en test runner. Echte benchmarks, Node.js-compatibiliteitsgaten, migratiepatronen en waar ik Bun vandaag in productie gebruik.

Delen:X / TwitterLinkedIn

Elke paar jaar krijgt het JavaScript-ecosysteem een nieuwe runtime en het discours volgt een voorspelbare boog. Hype. Benchmarks. "X is dood." Realiteitscheck. Landen bij de daadwerkelijke use cases waar de nieuwe tool echt uitblinkt.

Bun zit midden in die boog op dit moment. En in tegenstelling tot de meeste uitdagers, blijft het hangen. Niet omdat het "sneller" is (hoewel dat vaak wel zo is), maar omdat het een wezenlijk ander probleem oplost: de JavaScript-toolchain heeft te veel bewegende onderdelen, en Bun comprimeert ze tot een.

Ik gebruik Bun nu al ruim een jaar in diverse hoedanigheden. Sommige daarvan in productie. Sommige daarvan ter vervanging van tools waarvan ik dacht dat ik ze nooit zou vervangen. Dit bericht is een eerlijke verantwoording van wat werkt, wat niet, en waar de hiaten er nog toe doen.

Wat Bun Eigenlijk Is#

Het eerste misverstand om uit de weg te ruimen: Bun is niet "een snellere Node.js." Die framing doet het tekort.

Bun is vier tools in een binary:

  1. Een JavaScript/TypeScript runtime — draait je code, zoals Node.js of Deno
  2. Een package manager — vervangt npm, yarn of pnpm
  3. Een bundler — vervangt esbuild, webpack of Rollup voor bepaalde use cases
  4. Een test runner — vervangt Jest of Vitest voor de meeste testsuites

Het belangrijkste architecturale verschil met Node.js is de engine. Node.js gebruikt V8 (de engine van Chrome). Bun gebruikt JavaScriptCore (de engine van Safari). Beide zijn volwassen, productieklare engines, maar ze maken verschillende afwegingen. JavaScriptCore heeft doorgaans snellere opstarttijden en lagere geheugenoverhead. V8 heeft doorgaans betere piekdoorvoer voor langlopende berekeningen. In de praktijk zijn deze verschillen kleiner dan je zou denken voor de meeste workloads.

Het andere grote onderscheidende kenmerk: Bun is geschreven in Zig, een systeemprogrammeertaal die op ongeveer hetzelfde niveau zit als C maar met betere geheugensveiligheidsgaranties. Dit is waarom Bun zo agressief kan zijn met prestaties — Zig geeft je het soort low-level controle dat C biedt zonder de valkuilendichtheid van C.

bash
# Check your Bun version
bun --version
 
# Run a TypeScript file directly — no tsconfig, no compilation step
bun run server.ts
 
# Install packages
bun install
 
# Run tests
bun test
 
# Bundle for production
bun build ./src/index.ts --outdir ./dist

Dat is een binary die het werk doet van node + npm + esbuild + vitest. Of je het nu geweldig vindt of niet, het is een overtuigende reductie in complexiteit.

De Snelheidsclaims — Eerlijke Benchmarks#

Laat me direct zijn hierover: Bun's marketing-benchmarks zijn cherry-picked. Niet frauduleus — cherry-picked. Ze tonen de scenario's waar Bun het beste presteert, wat precies is wat je zou verwachten van marketingmateriaal. Het probleem is dat mensen extrapoleren uit die benchmarks om te beweren dat Bun "25x sneller" is in alles, wat het absoluut niet is.

Dit is waar Bun oprecht, merkbaar sneller is:

Opstarttijd#

Dit is Bun's grootste echte voordeel en het is niet eens in de buurt.

bash
# Measuring startup time — run each 100 times
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Typical results:
# node:  ~40ms
# bun:   ~6ms

Dat is ruwweg een 6-7x verschil in opstarttijd. Voor scripts, CLI-tools en serverless functions waar cold start ertoe doet, is dit significant. Voor een langlopend serverproces dat een keer start en wekenlang draait, is het irrelevant.

Pakketinstallatie#

Dit is het andere gebied waar Bun de concurrentie beschaamt.

bash
# Clean install benchmark — delete node_modules and lockfile first
rm -rf node_modules bun.lockb package-lock.json
 
# Time npm
time npm install
# Real: ~18.4s (typical medium-sized project)
 
# Time bun
time bun install
# Real: ~2.1s

Dat is een 8-9x verschil, en het is consistent. De redenen zijn voornamelijk:

  1. Binaire lockfilebun.lockb is een binair formaat, geen JSON. Sneller om te lezen en schrijven.
  2. Globale cache — Bun houdt een globale modulecache bij zodat herinstallaties tussen projecten gedownloade pakketten delen.
  3. Zig's I/O — De package manager zelf is geschreven in Zig, niet JavaScript. Bestandsoperaties zitten dichter bij de hardware.
  4. Symlink-strategie — Bun gebruikt hardlinks vanuit de globale cache in plaats van bestanden te kopieren.

HTTP Server-doorvoer#

De ingebouwde HTTP-server van Bun is snel, maar de vergelijkingen hebben context nodig.

bash
# Quick and dirty benchmark with bombardier
# Testing a simple "Hello World" response
 
# Bun server
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sec: ~105,000
 
# Node.js (native http module)
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 versus rauwe Node.js: ruwweg 2x voor triviale responses. Bun versus Express: ruwweg 7x, maar dat is oneerlijk omdat Express middleware-overhead toevoegt. Het moment dat je echte logica toevoegt — databasequery's, authenticatie, JSON-serialisatie van werkelijke data — wordt de kloof aanzienlijk kleiner.

Waar Het Verschil Verwaarloosbaar Is#

CPU-gebonden berekeningen:

typescript
// fibonacci.ts — this is engine-bound, not runtime-bound
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) wint hier zelfs licht. V8's JIT-compiler is agressiever op hot loops. Voor CPU-gebonden werk zijn de engineverschillen gelijk op — soms wint V8, soms wint JSC, en de verschillen zitten binnen de ruis.

Hoe Je Je Eigen Benchmarks Uitvoert#

Vertrouw niemands benchmarks, inclusief de mijne. Zo meet je wat ertoe doet voor jouw specifieke workload:

bash
# Install hyperfine for proper benchmarking
brew install hyperfine  # macOS
# or: cargo install hyperfine
 
# Benchmark startup + execution of your actual app
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# For HTTP servers, use bombardier or wrk
# Important: test with realistic payloads, not "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# Memory comparison
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

De vuistregel: als je bottleneck I/O is (bestandssysteem, netwerk, database), is Bun's voordeel bescheiden. Als je bottleneck opstarttijd of toolchain-snelheid is, wint Bun ruimschoots. Als je bottleneck ruwe berekening is, is het een toss-up.

Bun als Package Manager#

Dit is waar ik volledig ben overgestapt. Zelfs op projecten waar ik Node.js in productie draai, gebruik ik bun install voor lokale ontwikkeling en CI. Het is gewoon sneller, en de compatibiliteit is uitstekend.

De Basis#

bash
# Install all dependencies from package.json
bun install
 
# Add a dependency
bun add express
 
# Add a dev dependency
bun add -d vitest
 
# Remove a dependency
bun remove express
 
# Update a dependency
bun update express
 
# Install a specific version
bun add express@4.18.2

Als je npm of yarn hebt gebruikt, is dit volledig vertrouwd. De vlaggen zijn iets anders (-d in plaats van --save-dev), maar het mentale model is identiek.

De Lockfile-situatie#

Bun gebruikt bun.lockb, een binaire lockfile. Dit is zowel zijn superkracht als zijn grootste wrijvingspunt.

Het goede: Het is dramatisch sneller om te lezen en schrijven. Het binaire formaat betekent dat Bun de lockfile in microseconden kan parsen, niet de honderden milliseconden die npm besteedt aan het parsen van package-lock.json.

Het slechte: Je kunt het niet beoordelen in een diff. Als je in een team zit en iemand een dependency bijwerkt, kun je niet naar de lockfile-diff in een PR kijken en zien wat er is veranderd. Dit doet er meer toe dan snelheidsvoorstanders willen toegeven.

bash
# You can dump the lockfile to human-readable format
bun bun.lockb > lockfile-dump.txt
 
# Or use the built-in text output
bun install --yarn
# This generates a yarn.lock alongside bun.lockb

Mijn aanpak: ik commit bun.lockb naar de repo en genereer ook een yarn.lock of package-lock.json als leesbare terugvaloptie. Riem en bretels.

Workspace-ondersteuning#

Bun ondersteunt npm/yarn-stijl workspaces:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Install dependencies for all workspaces
bun install
 
# Run a script in a specific workspace
bun run --filter packages/shared build
 
# Add a dependency to a specific workspace
bun add react --filter apps/web

Workspace-ondersteuning is solide en aanzienlijk verbeterd. Het belangrijkste verschil met pnpm is dat Bun's workspace-afhankelijkheidsresolutie minder strikt is — pnpm's striktheid is een feature voor monorepo's omdat het phantom dependencies detecteert.

Compatibiliteit Met Bestaande Projecten#

Je kunt bun install in vrijwel elk bestaand Node.js-project droppen. Het leest package.json, respecteert .npmrc voor registry-configuratie en gaat correct om met peerDependencies. De transitie is doorgaans:

bash
# Step 1: Delete existing lockfile and node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# Step 2: Install with Bun
bun install
 
# Step 3: Verify your app still works
bun run dev
# or: node dist/server.js  (Bun package manager, Node runtime)

Ik heb dit op een dozijn projecten gedaan en nul problemen gehad met de package manager zelf. De enige valkuil is als je CI-pipeline specifiek zoekt naar package-lock.json — dan moet je die bijwerken om bun.lockb te verwerken.

Node.js-compatibiliteit#

Dit is de sectie waar ik het meest voorzichtig moet zijn, omdat de situatie elke maand verandert. Per begin 2026, dit is het eerlijke beeld.

Wat Werkt#

De overgrote meerderheid van npm-pakketten werkt zonder aanpassingen. Bun implementeert de meeste ingebouwde Node.js-modules:

typescript
// These all work as expected 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";

Zowel CommonJS als ESM werken. require() en import kunnen naast elkaar bestaan. TypeScript draait zonder enige compilatiestap — Bun verwijdert types bij het parsen.

Frameworks die werken:

  • Express — werkt, inclusief middleware-ecosysteem
  • Fastify — werkt
  • Hono — werkt (en is uitstekend met Bun)
  • Next.js — werkt met kanttekeningen (hieronder meer)
  • Prisma — werkt
  • Drizzle ORM — werkt
  • Socket.io — werkt

Wat Niet Werkt (of Problemen Heeft)#

De hiaten vallen doorgaans in een paar categorieen:

Native addons (node-gyp): Als een pakket C++ addons gebruikt gecompileerd met node-gyp, werkt het mogelijk niet met Bun. Bun heeft zijn eigen FFI-systeem en ondersteunt veel native modules, maar de dekking is niet 100%. Bijvoorbeeld, bcrypt (de native versie) heeft problemen gehad — gebruik in plaats daarvan bcryptjs.

bash
# Check if a package uses native addons
ls node_modules/your-package/binding.gyp  # If this exists, it's native

Specifieke Node.js internals: Sommige pakketten reiken in Node.js internals zoals process.binding() of gebruiken V8-specifieke API's. Deze werken niet in Bun aangezien het op JavaScriptCore draait.

typescript
// This will NOT work in Bun — V8-specific
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// This WILL work — use Bun's equivalent or a cross-runtime approach
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker threads: Bun ondersteunt Web Workers en node:worker_threads, maar er zijn randgevallen. Sommige geavanceerde gebruikspatronen — vooral rond SharedArrayBuffer en Atomics — kunnen zich anders gedragen.

vm-module: node:vm heeft gedeeltelijke ondersteuning. Als je code of een dependency uitgebreid vm.createContext() gebruikt (sommige template-engines doen dat), test dan grondig.

De Compatibiliteitstracker#

Bun onderhoudt een officiele compatibiliteitstracker. Controleer deze voordat je je committeert aan Bun voor een project:

bash
# Run Bun's built-in compatibility check on your project
bun --bun node_modules/.bin/your-tool
 
# The --bun flag forces Bun's runtime even for node_modules scripts

Mijn aanbeveling: ga niet uit van compatibiliteit. Draai je testsuite onder Bun voordat je beslist. Het kost vijf minuten en bespaart uren debuggen.

bash
# Quick compatibility check — run your full test suite under Bun
bun test  # If you use bun test runner
# or
bun run vitest  # If you use vitest

Bun's Ingebouwde API's#

Dit is waar Bun interessant wordt. In plaats van alleen Node.js API's opnieuw te implementeren, biedt Bun zijn eigen API's die ontworpen zijn om eenvoudiger en sneller te zijn.

Bun.serve() — De Ingebouwde HTTP-server#

Dit is de API die ik het meest gebruik. Het is schoon, snel, en WebSocket-ondersteuning zit er direct in.

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}`);

Een paar dingen om op te merken:

  1. Web-standaard Request/Response — geen proprietaire API. De fetch-handler ontvangt een standaard Request en retourneert een standaard Response. Als je een Cloudflare Worker hebt geschreven, voelt dit identiek.
  2. Response.json() — ingebouwde JSON-response helper.
  3. Geen import nodigBun.serve is een global. Geen require("http").

Hier is een realistischer voorbeeld met routing, JSON-body parsing en foutafhandeling:

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}`);

Dat is een volledige CRUD API met SQLite in ongeveer 50 regels. Geen Express, geen ORM, geen middleware-keten. Voor kleine API's en interne tools is dit nu mijn standaard-setup.

Bun.file() en Bun.write() — Bestands-I/O#

Bun's bestands-API is verfrissend eenvoudig vergeleken met fs.readFile():

typescript
// Reading files
const file = Bun.file("./config.json");
const text = await file.text();       // Read as string
const json = await file.json();       // Parse as JSON directly
const bytes = await file.arrayBuffer(); // Read as ArrayBuffer
const stream = file.stream();          // Read as ReadableStream
 
// File metadata
console.log(file.size);  // Size in bytes
console.log(file.type);  // MIME type (e.g., "application/json")
 
// Writing files
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"));
 
// Write a Response body to a file
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

De Bun.file() API is lazy — het leest het bestand pas wanneer je .text(), .json(), etc. aanroept. Dit betekent dat je Bun.file() referenties kunt doorgeven zonder I/O-kosten te maken totdat je de data daadwerkelijk nodig hebt.

Ingebouwde WebSocket-ondersteuning#

WebSockets zijn eersteklas 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) {
      // Broadcast to all subscribers
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

Het server.publish() en ws.subscribe() patroon is ingebouwde pub/sub. Geen Redis, geen aparte WebSocket-bibliotheek. Voor eenvoudige realtime-functionaliteiten is dit ongelooflijk handig.

Ingebouwde SQLite met bun:sqlite#

Dit verraste me het meest. Bun wordt geleverd met SQLite direct ingebouwd in de runtime:

typescript
import { Database } from "bun:sqlite";
 
// Open or create a database
const db = new Database("myapp.db");
 
// WAL mode for better concurrent read performance
db.exec("PRAGMA journal_mode = WAL");
 
// Create tables
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 (reusable, faster for repeated queries)
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
 
const findByEmail = db.prepare(
  "SELECT * FROM users WHERE email = $email"
);
 
// Usage
const user = insertUser.get({
  $email: "alice@example.com",
  $name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
 
// Transactions
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`);

Dit is synchrone SQLite met de prestaties van een C-bibliotheek (want dat is het — Bun embedt libsqlite3 direct). Voor CLI-tools, local-first apps en kleine services betekent ingebouwde SQLite nul externe afhankelijkheden voor je datalaag.

Bun Test Runner#

bun test is een drop-in vervanging voor Jest in de meeste gevallen. Het gebruikt dezelfde describe/it/expect API en ondersteunt de meeste Jest-matchers.

Basisgebruik#

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
# Run all tests
bun test
 
# Run specific file
bun test math.test.ts
 
# Run tests matching a pattern
bun test --test-name-pattern "adds numbers"
 
# Watch mode
bun test --watch
 
# Coverage
bun test --coverage

Mocking#

Bun ondersteunt Jest-compatibele mocking:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Mock a module
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");
  });
});
 
// Spy on an object method
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 versus Vitest — Mijn Eerlijke Vergelijking#

Ik gebruik Vitest voor dit project (en de meeste van mijn projecten). Dit is waarom ik niet volledig ben overgestapt:

Waar bun test wint:

  • Opstartsnelheid. bun test begint tests uit te voeren sneller dan Vitest zijn configuratie kan laden.
  • Nul configuratie. Geen vitest.config.ts nodig voor basale setups.
  • Ingebouwde TypeScript. Geen transformatiestap.

Waar Vitest nog steeds wint:

  • Ecosysteem: Vitest heeft meer plugins, betere IDE-integratie en een grotere community.
  • Configuratie: Het configuratiesysteem van Vitest is flexibeler. Aangepaste reporters, complexe setup-bestanden, meerdere testomgevingen.
  • Browsermodus: Vitest kan tests in een echte browser draaien. Bun niet.
  • Compatibiliteit: Sommige testbibliotheken (Testing Library, MSW) zijn grondiger getest met Vitest/Jest.
  • Snapshot-testing: Beide ondersteunen het, maar Vitest's implementatie is volwassener met betere diff-uitvoer.

Voor een nieuw project met eenvoudige testbehoeften zou ik bun test gebruiken. Voor een bestaand project met Testing Library, MSW en complexe mocking, houd ik Vitest.

Bun Bundler#

bun build is een snelle JavaScript/TypeScript bundler. Het is geen webpack-vervanging — het zit meer in de categorie esbuild: snel, eigenwijs en gericht op de gangbare gevallen.

Basis Bundling#

bash
# Bundle a single entry point
bun build ./src/index.ts --outdir ./dist
 
# Bundle for different targets
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
 
# Minify
bun build ./src/index.ts --outdir ./dist --minify
 
# Generate sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap external

Programmatische 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"],  // Don't bundle these
  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`);
}

Tree-Shaking#

Bun ondersteunt tree-shaking voor ESM:

typescript
// utils.ts
export function used() {
  return "I'll be in the bundle";
}
 
export function unused() {
  return "I'll be tree-shaken away";
}
 
// index.ts
import { used } from "./utils";
console.log(used());
bash
bun build ./src/index.ts --outdir ./dist --minify
# The `unused` function won't appear in the output

Waar Bun Build Tekortschiet#

  • Geen CSS-bundling — Je hebt een apart gereedschap nodig voor CSS (PostCSS, Lightning CSS, Tailwind CLI).
  • Geen HTML-generatie — Het bundelt JavaScript/TypeScript, geen complete webapps.
  • Plugin-ecosysteem — esbuild heeft een veel groter plugin-ecosysteem. Bun's plugin-API is compatibel maar de community is kleiner.
  • Geavanceerde code splitting — Webpack en Rollup bieden nog steeds meer geavanceerde chunk-strategieen.

Voor het bouwen van een bibliotheek of de JS-bundle van een eenvoudige webapp is bun build uitstekend. Voor complexe app-builds met CSS-modules, beeldoptimalisatie en aangepaste chunk-strategieen, wil je nog steeds een volledige bundler.

Bun Macro's#

Een echt unieke feature: compile-time code-uitvoering via macro's.

typescript
// build-info.ts — this file runs at BUILD TIME, not runtime
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() executes at bundle time
// The result is inlined as a static value
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

Na het bundelen wordt getBuildInfo() vervangen door het letterlijke object — geen functieaanroep tijdens runtime, geen import van child_process. De code is tijdens de build uitgevoerd en het resultaat is ge-inlined. Dit is krachtig voor het insluiten van build-metadata, feature flags of omgevingsspecifieke configuratie.

Bun Gebruiken met Next.js#

Dit is de vraag die ik het vaakst krijg, dus laat me heel specifiek zijn.

Wat Vandaag Werkt#

Bun als package manager voor Next.js — werkt perfect:

bash
# Use Bun to install dependencies, then use Node.js to run Next.js
bun install
bun run dev    # This actually runs the "dev" script via Node.js by default
bun run build
bun run start

Dit is wat ik doe voor elk Next.js-project. Het bun run <script> commando leest de scripts-sectie van package.json en voert het uit. Standaard gebruikt het de Node.js van het systeem voor de daadwerkelijke uitvoering. Je krijgt Bun's snelle pakketinstallatie zonder je runtime te veranderen.

Bun-runtime voor Next.js-ontwikkeling:

bash
# Force Next.js to run under Bun's runtime
bun --bun run dev

Dit werkt voor ontwikkeling in de meeste gevallen. De --bun vlag vertelt Bun om zijn eigen runtime te gebruiken in plaats van te delegeren naar Node.js. Hot module replacement werkt. API-routes werken. Server components werken.

Wat Nog Experimenteel Is#

Bun-runtime voor Next.js-productiebuilds:

bash
# Build with Bun runtime
bun --bun run build
 
# Start production server with Bun runtime
bun --bun run start

Dit werkt voor veel projecten maar ik ben randgevallen tegengekomen:

  1. Sommig middleware-gedrag verschilt — Als je Next.js middleware gebruikt die afhankelijk is van Node.js-specifieke API's, kun je compatibiliteitsproblemen tegenkomen.
  2. Beeldoptimalisatie — Next.js' beeldoptimalisatiepipeline gebruikt sharp, wat een native addon is. Het werkt met Bun, maar ik heb incidentele problemen gezien.
  3. ISR (Incremental Static Regeneration) — Werkt, maar ik heb het niet onder druk getest met Bun in productie.

Mijn Aanbeveling voor Next.js#

Gebruik Bun als package manager. Gebruik Node.js als runtime. Dit geeft je de snelheidsvoordelen van bun install zonder enig compatibiliteitsrisico.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Daily workflow
bun install      # Fast package installation
bun run dev      # Runs "next dev" via Node.js
bun run build    # Runs "next build" via Node.js

Wanneer Bun's Node.js-compatibiliteit 100% bereikt voor Next.js' intern gebruik (het is dichtbij, maar nog niet zover), stap ik over. Tot die tijd bespaart alleen al de package manager me genoeg tijd om de installatie te rechtvaardigen.

Docker met Bun#

Het officiele Bun Docker-image is goed onderhouden en productie-klaar.

Basis Dockerfile#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# Install dependencies
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# Build (if needed)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
 
# Production
FROM base AS production
WORKDIR /app
 
# Don't run as root
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"]

Multi-Stage Build voor Minimaal Image#

dockerfile
# Build stage: full Bun image with all dependencies
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build ./src/index.ts --target bun --outdir ./dist --minify
 
# Runtime stage: smaller base image
FROM oven/bun:1-slim AS runtime
WORKDIR /app
 
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 appuser
USER appuser
 
COPY --from=builder /app/dist ./dist
 
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]

Compileren naar een Enkele Binary#

Dit is een van Bun's killer features voor deployment:

bash
# Compile your app into a single executable
bun build --compile ./src/server.ts --outfile server
 
# The output is a standalone binary — no Bun or Node.js needed to run it
./server
dockerfile
# Ultra-minimal Docker image using compiled 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
 
# Final image — just the 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"]

De gecompileerde binary is doorgaans 50-90 MB (het bundelt de Bun-runtime). Dat is groter dan een Go-binary maar veel kleiner dan een volledige Node.js-installatie plus node_modules. Voor gecontaineriseerde deployments is het op-zichzelf-staande karakter een significante vereenvoudiging.

Groottevergelijking#

bash
# Node.js image
docker images | grep node
# node:20-slim    ~180MB
 
# Bun image
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# Compiled binary on debian:bookworm-slim
# ~80MB base + ~70MB binary = ~150MB total
 
# vs. Alpine with Node.js
# node:20-alpine   ~130MB + node_modules

De binary-aanpak elimineert node_modules volledig uit het eindimage. Geen npm install in productie. Geen supply chain-aanvalsoppervlak van honderden pakketten. Slechts een bestand.

Migratiepatronen#

Als je overweegt om naar Bun te migreren, dit is het incrementele pad dat ik aanbeveel:

Fase 1: Alleen Package Manager (Nul Risico)#

bash
# Replace npm/yarn/pnpm with bun install
# Change your CI pipeline:
# Before:
npm ci
 
# After:
bun install --frozen-lockfile

Geen codewijzigingen. Geen runtime-wijzigingen. Gewoon snellere installaties. Als er iets breekt (dat gebeurt niet), revert door bun.lockb te verwijderen en npm install uit te voeren.

Fase 2: Scripts en Tooling#

bash
# Use bun to run development scripts
bun run dev
bun run lint
bun run format
 
# Use bun for one-off scripts
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

Nog steeds Node.js als runtime voor je daadwerkelijke applicatie. Maar scripts profiteren van Bun's snellere opstarttijd en native TypeScript-ondersteuning.

Fase 3: Test Runner (Gemiddeld Risico)#

bash
# Replace vitest/jest with bun test for simple test suites
bun test
 
# Keep vitest for complex test setups
# (Testing Library, MSW, custom environments)

Draai je volledige testsuite onder bun test. Als alles slaagt, heb je een devDependency geelimineerd. Als sommige tests falen door compatibiliteit, houd dan Vitest voor die tests en gebruik bun test voor de rest.

Fase 4: Runtime voor Nieuwe Services (Berekend Risico)#

typescript
// New microservices or APIs — start with Bun from day one
Bun.serve({
  port: 3000,
  fetch(req) {
    // Your new service here
  },
});

Migreer geen bestaande Node.js-services naar de Bun-runtime. Schrijf in plaats daarvan nieuwe services met Bun vanaf het begin. Dit beperkt je blast radius.

Fase 5: Runtime-migratie (Geavanceerd)#

bash
# Only after thorough testing:
# Replace node with bun for existing services
# Before:
node dist/server.js
 
# After:
bun dist/server.js

Ik raad dit alleen aan voor services met uitstekende testdekking. Draai je belastingtests onder Bun voordat je productie overschakelt.

Omgevingsvariabelen en Configuratie#

Bun verwerkt .env-bestanden automatisch — geen dotenv-pakket nodig:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// These are available without any import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun-specific alternative

Bun laadt .env, .env.local, .env.production, etc. automatisch, volgens dezelfde conventie als Next.js. Een afhankelijkheid minder in je package.json.

Foutafhandeling en Debugging#

Bun's foutuitvoer is aanzienlijk verbeterd, maar het is in sommige gevallen nog steeds niet zo gepolijst als Node.js:

bash
# Bun's debugger — works with VS Code
bun --inspect run server.ts
 
# Bun's inspect-brk — pause on first line
bun --inspect-brk run server.ts

Voor VS Code, voeg dit toe aan je .vscode/launch.json:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "bun",
      "request": "launch",
      "name": "Debug Bun",
      "program": "${workspaceFolder}/src/server.ts",
      "cwd": "${workspaceFolder}",
      "stopOnEntry": false,
      "watchMode": false
    }
  ]
}

Stack traces in Bun zijn over het algemeen nauwkeurig en bevatten source maps voor TypeScript. Het belangrijkste gat in debugging is dat sommige Node.js-specifieke debugging tools (zoals ndb of clinic.js) niet werken met Bun.

Beveiligingsoverwegingen#

Een paar dingen om over na te denken als je Bun evalueert voor productie:

Volwassenheid: Node.js draait al 15+ jaar in productie. Elk randgeval in HTTP-parsing, TLS-afhandeling en stream-verwerking is gevonden en opgelost. Bun is jonger. Het is goed getest, maar het oppervlak voor onontdekte bugs is groter.

Beveiligingspatches: Het Bun-team levert frequent updates, maar het Node.js-beveiligingsteam heeft een formeel CVE-proces, gecoordineerde disclosure en een langere staat van dienst. Voor beveiligingskritieke applicaties doet dit ertoe.

Supply chain: Bun's ingebouwde features (SQLite, HTTP-server, WebSockets) betekenen minder npm-afhankelijkheden. Minder afhankelijkheden betekent een kleiner supply chain-aanvalsoppervlak. Dit is een oprecht beveiligingsvoordeel.

bash
# Compare dependency counts
# A typical Express + SQLite + WebSocket project:
npm ls --all | wc -l
# ~340 packages
 
# The same functionality with Bun built-ins:
bun pm ls --all | wc -l
# ~12 packages (just your application code)

Dat is een betekenisvolle reductie in het aantal pakketten dat je vertrouwt met je productie-workload.

Prestatieoptimalisatie#

Een paar Bun-specifieke prestatietips:

typescript
// Use Bun.serve() options for production tuning
Bun.serve({
  port: 3000,
 
  // Increase max request body size (default is 128MB)
  maxRequestBodySize: 1024 * 1024 * 50, // 50MB
 
  // Enable development mode for better error pages
  development: process.env.NODE_ENV !== "production",
 
  // Reuse port (useful for zero-downtime restarts)
  reusePort: true,
 
  fetch(req) {
    return new Response("OK");
  },
});
typescript
// Use Bun.Transpiler for runtime code transformation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  target: "browser",
});
 
const code = transpiler.transformSync(`
  const App: React.FC = () => <div>Hello</div>;
  export default App;
`);
bash
# Bun's memory usage flags
bun --smol run server.ts  # Reduce memory footprint (slightly slower)
 
# Set max heap size
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512MB limit

Veelvoorkomende Valkuilen#

Na een jaar Bun te gebruiken, dit zijn de dingen die me hebben doen struikelen:

1. Globaal Fetch-gedrag Verschilt#

typescript
// Node.js 18+ fetch and Bun's fetch are slightly different
// in how they handle certain headers and redirects
 
// Bun follows redirects by default (like browsers)
// Node.js fetch also follows redirects, but the behavior
// with certain status codes (303, 307, 308) can differ
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // Be explicit about redirect handling
});

2. Proces-exitgedrag#

typescript
// Bun exits when the event loop is empty
// Node.js sometimes keeps running due to lingering handles
 
// If your Bun script exits unexpectedly, something isn't
// keeping the event loop alive
 
// This will exit immediately in Bun:
setTimeout(() => {}, 0);
 
// This will keep running:
setTimeout(() => {}, 1000);
// (Bun exits after the timeout fires)

3. TypeScript-configuratie#

typescript
// Bun has its own tsconfig defaults
// If you're sharing a project between Bun and Node.js,
// be explicit in your tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Add Bun type definitions
  }
}
bash
# Install Bun types
bun add -d @types/bun

4. Hot Reload in Ontwikkeling#

bash
# Bun has built-in watch mode
bun --watch run server.ts
 
# This restarts the process on file changes
# It's not HMR (Hot Module Replacement) — it's a full restart
# But because Bun starts so fast, it feels instant

5. Het bunfig.toml Configuratiebestand#

toml
# bunfig.toml — Bun's config file (optional)
 
[install]
# Use a private registry
registry = "https://npm.mycompany.com"
 
# Scoped registries
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# Test configuration
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# Shell to use for bun run
shell = "bash"

Mijn Oordeel#

Na een jaar productiegebruik, dit is waar ik ben uitgekomen:

Waar Ik Bun Vandaag Gebruik#

Package manager voor alle projecten — inclusief dit Next.js-blog. bun install is sneller, en de compatibiliteit is in wezen perfect. Ik zie geen reden meer om npm of yarn te gebruiken. pnpm is het enige alternatief dat ik zou overwegen (vanwege de strikte afhankelijkheidsresolutie in monorepo's).

Runtime voor scripts en CLI-tools — Elk TypeScript-bestand dat ik een keer moet uitvoeren, draai ik met bun. Geen compilatiestap. Snelle opstart. Ingebouwde .env-loading. Het heeft ts-node en tsx volledig vervangen in mijn workflow.

Runtime voor kleine API's en interne toolsBun.serve() + bun:sqlite is een ongelooflijk productieve stack voor interne tools, webhook-handlers en kleine services. Het "een binary, geen afhankelijkheden" deployment-model is overtuigend.

Test runner voor eenvoudige projecten — Voor projecten met eenvoudige testbehoeften is bun test snel en vereist nul configuratie.

Waar Ik Bij Node.js Blijf#

Productie-Next.js — Niet omdat Bun niet werkt, maar omdat de risico-opbrengstverhouding het nog niet rechtvaardigt. Next.js is een complex framework met veel integratiepunten. Ik wil de meest beproefde runtime eronder.

Kritieke productieservices — Mijn belangrijkste API-servers draaien Node.js achter PM2. Het monitoring-ecosysteem, de debugging-tools, de operationele kennis — het is allemaal Node.js. Bun komt er wel, maar is er nog niet.

Alles met native addons — Als een afhankelijkheidsketen C++ native addons bevat, probeer ik Bun niet eens. Het is de moeite niet waard om compatibiliteitsproblemen te debuggen.

Teams die niet vertrouwd zijn met Bun — Bun als runtime introduceren bij een team dat het nooit heeft gebruikt, voegt cognitieve overhead toe. Als package manager, prima. Als runtime, wacht tot het team er klaar voor is.

Wat Ik in de Gaten Houd#

Bun's compatibiliteitstracker — Wanneer het 100% bereikt voor de Node.js-API's die ik gebruik, beoordeel ik het opnieuw.

Framework-ondersteuning — Next.js, Remix en SvelteKit hebben allemaal verschillende niveaus van Bun-ondersteuning. Wanneer een van hen officieel Bun ondersteunt als productie-runtime, is dat een signaal.

Enterprise-adoptie — Zodra bedrijven met echte SLA's Bun in productie draaien en erover schrijven, is de volwassenheidsvraag beantwoord.

De 1.2+ release-lijn — Bun beweegt snel. Elke week landen er features. De Bun die ik vandaag gebruik is aanzienlijk beter dan de Bun die ik een jaar geleden probeerde.

Afronden#

Bun is geen wondermiddel. Het maakt een langzame app niet snel en het maakt een slecht ontworpen API niet goed ontworpen. Maar het is een oprechte verbetering in ontwikkelervaring voor het JavaScript-ecosysteem.

Wat ik het meest waardeer aan Bun is niet een enkele feature. Het is de reductie in toolchain-complexiteit. Een binary die pakketten installeert, TypeScript draait, code bundelt en tests uitvoert. Geen tsconfig.json voor scripts. Geen Babel. Geen aparte test runner-configuratie. Gewoon bun run your-file.ts en het werkt.

Het praktische advies: begin met bun install. Het is nul risico, onmiddellijk voordeel. Probeer dan bun run voor scripts. Evalueer vervolgens de rest op basis van je specifieke behoeften. Je hoeft niet all-in te gaan. Bun werkt prima als gedeeltelijke vervanging, en dat is waarschijnlijk hoe de meeste mensen het vandaag zouden moeten gebruiken.

Gerelateerde artikelen