Gå till innehåll
·24 min läsning

Bun i produktion: Vad som fungerar, vad som inte gör det och vad som överraskade mig

Bun som runtime, pakethanterare, bundler och testkörare. Riktiga benchmarks, Node.js-kompatibilitetsluckor, migrationsmönster och var jag använder Bun i produktion idag.

Dela:X / TwitterLinkedIn

Med några års mellanrum får JavaScript-ekosystemet en ny runtime och diskursen följer en förutsägbar kurva. Hype. Benchmarks. "X är dött." Verklighetskontroll. Att landa i de faktiska användningsfallen där det nya verktyget genuint lyser.

Bun befinner sig mitt i den kurvan just nu. Och till skillnad från de flesta utmanare stannar det kvar. Inte för att det är "snabbare" (även om det ofta är det), utan för att det löser ett genuint annorlunda problem: JavaScript-verktygskedjan har för många rörliga delar, och Bun kollapsar dem till en enda.

Jag har använt Bun i olika sammanhang i över ett år nu. Delvis i produktion. Delvis genom att ersätta verktyg jag trodde att jag aldrig skulle byta ut. Det här inlägget är en ärlig redovisning av vad som fungerar, vad som inte gör det och var luckorna fortfarande spelar roll.

Vad Bun faktiskt är#

Det första missförståndet att reda ut: Bun är inte "en snabbare Node.js." Den inramningen undersäljer det.

Bun är fyra verktyg i en enda binärfil:

  1. En JavaScript/TypeScript-runtime — kör din kod, som Node.js eller Deno
  2. En pakethanterare — ersätter npm, yarn eller pnpm
  3. En bundler — ersätter esbuild, webpack eller Rollup för vissa användningsfall
  4. En testkörare — ersätter Jest eller Vitest för de flesta testsviter

Den viktigaste arkitektoniska skillnaden mot Node.js är motorn. Node.js använder V8 (Chromes motor). Bun använder JavaScriptCore (Safaris motor). Båda är mogna produktionsklara motorer, men de gör olika avvägningar. JavaScriptCore tenderar att ha snabbare starttider och lägre minnesanvändning. V8 tenderar att ha bättre topprestanda för långvariga beräkningar. I praktiken är dessa skillnader mindre än man tror för de flesta arbetsbelastningar.

Den andra stora differentieraren: Bun är skrivet i Zig, ett systemprogrammeringsspråk som befinner sig på ungefär samma nivå som C men med bättre minnesäkerhetsgarantier. Det är därför Bun kan vara så aggressivt med prestanda — Zig ger dig den typ av lågnivåkontroll som C erbjuder utan C:s höga risk för minnesbuggar.

bash
# Kontrollera din Bun-version
bun --version
 
# Kör en TypeScript-fil direkt — ingen tsconfig, inget kompileringssteg
bun run server.ts
 
# Installera paket
bun install
 
# Kör tester
bun test
 
# Bundla för produktion
bun build ./src/index.ts --outdir ./dist

Det är en enda binärfil som gör jobbet för node + npm + esbuild + vitest. Älska det eller hata det, det är en övertygande minskning av komplexitet.

Hastighetsargumenten — Ärliga benchmarks#

Låt mig vara direkt om det här: Buns marknadsföringsbenchmarks är selektivt utvalda. Inte bedrägliga — selektivt utvalda. De visar scenarierna där Bun presterar bäst, vilket är exakt vad man förväntar sig av marknadsföringsmaterial. Problemet är att folk extrapolerar från dessa benchmarks och hävdar att Bun är "25x snabbare" på allt, vilket det absolut inte är.

Här är var Bun är genuint, meningsfullt snabbare:

Starttid#

Det här är Buns största genuina fördel och det är inte ens i närheten.

bash
# Mätning av starttid — kör varje 100 gånger
hyperfine --warmup 5 'node -e "console.log(1)"' 'bun -e "console.log(1)"'
 
# Typiska resultat:
# node:  ~40ms
# bun:   ~6ms

Det är ungefär en 6-7x skillnad i starttid. För skript, CLI-verktyg och serverlösa funktioner där kallstart spelar roll är detta betydande. För en långvarig serverprocess som startar en gång och kör i veckor är det irrelevant.

Paketinstallation#

Det här är det andra området där Bun utklassar konkurrensen.

bash
# Benchmark för ren installation — radera node_modules och lockfil först
rm -rf node_modules bun.lockb package-lock.json
 
# Tidsätt npm
time npm install
# Real: ~18.4s (typiskt medelstort projekt)
 
# Tidsätt bun
time bun install
# Real: ~2.1s

Det är en 8-9x skillnad, och den är konsekvent. Orsakerna är primärt:

  1. Binär lockfilbun.lockb är ett binärt format, inte JSON. Snabbare att läsa och skriva.
  2. Global cache — Bun upprätthåller en global modulcache så ominstallationer mellan projekt delar nedladdade paket.
  3. Zigs I/O — Pakethanteraren är skriven i Zig, inte JavaScript. Fil-I/O-operationer är närmare hårdvaran.
  4. Symlänkstrategi — Bun använder hårdlänkar från den globala cachen istället för att kopiera filer.

HTTP-serverprestanda#

Buns inbyggda HTTP-server är snabb, men jämförelserna behöver kontext.

bash
# Snabbt och enkelt benchmark med bombardier
# Testar en enkel "Hello World"-respons
 
# Bun-server
bombardier -c 100 -d 10s http://localhost:3000
# Requests/sek: ~105 000
 
# Node.js (nativa http-modulen)
bombardier -c 100 -d 10s http://localhost:3001
# Requests/sek: ~48 000
 
# Node.js (Express)
bombardier -c 100 -d 10s http://localhost:3002
# Requests/sek: ~15 000

Bun mot rå Node.js: ungefär 2x för triviala responser. Bun mot Express: ungefär 7x, men det är orättvist eftersom Express lägger till middleware-overhead. I samma stund som du lägger till riktig logik — databasfrågor, autentisering, JSON-serialisering av faktisk data — minskar gapet dramatiskt.

Där skillnaden är försumbar#

CPU-bunden beräkning:

typescript
// fibonacci.ts — detta är motorbundet, inte runtimebundet
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) vinner faktiskt marginellt här. V8:s JIT-kompilator är mer aggressiv på heta loopar. För CPU-bundet arbete är motorskillnaderna jämna — ibland vinner V8, ibland vinner JSC, och skillnaderna ligger inom felmarginalerna.

Hur du kör dina egna benchmarks#

Lita inte på någons benchmarks, inklusive mina. Så här mäter du det som spelar roll för just din arbetsbelastning:

bash
# Installera hyperfine för ordentlig benchmarking
brew install hyperfine  # macOS
# eller: cargo install hyperfine
 
# Benchmarka starttid + exekvering av din faktiska app
hyperfine --warmup 3 \
  'node dist/server.js' \
  'bun src/server.ts' \
  --prepare 'sleep 0.1'
 
# För HTTP-servrar, använd bombardier eller wrk
# Viktigt: testa med realistiska payloads, inte "Hello World"
bombardier -c 50 -d 30s -l http://localhost:3000/api/users
 
# Minnesjämförelse
/usr/bin/time -v node server.js  # Linux
/usr/bin/time -l bun server.ts   # macOS

Tumregeln: om din flaskhals är I/O (filsystem, nätverk, databas) är Buns fördel blygsam. Om din flaskhals är starttid eller verktygskedjans hastighet vinner Bun stort. Om din flaskhals är rå beräkning är det jämnt.

Bun som pakethanterare#

Det här är där jag har bytt helt. Även i projekt där jag kör Node.js i produktion använder jag bun install för lokal utveckling och CI. Det är helt enkelt snabbare, och kompatibiliteten är utmärkt.

Grunderna#

bash
# Installera alla beroenden från package.json
bun install
 
# Lägg till ett beroende
bun add express
 
# Lägg till ett dev-beroende
bun add -d vitest
 
# Ta bort ett beroende
bun remove express
 
# Uppdatera ett beroende
bun update express
 
# Installera en specifik version
bun add express@4.18.2

Om du har använt npm eller yarn är detta helt bekant. Flaggorna skiljer sig något (-d istället för --save-dev), men den mentala modellen är identisk.

Lockfil-situationen#

Bun använder bun.lockb, en binär lockfil. Det är både dess superkraft och dess största friktionspunkt.

Det positiva: Den är dramatiskt snabbare att läsa och skriva. Det binära formatet innebär att Bun kan tolka lockfilen på mikrosekunder, inte de hundratals millisekunder som npm spenderar på att parsa package-lock.json.

Det negativa: Du kan inte granska den i en diff. Om du jobbar i ett team och någon uppdaterar ett beroende kan du inte titta på lockfil-diffen i en PR och se vad som ändrades. Det spelar större roll än hastighetsförespråkare vill erkänna.

bash
# Du kan dumpa lockfilen till läsbart format
bun bun.lockb > lockfile-dump.txt
 
# Eller använd den inbyggda textutmatningen
bun install --yarn
# Detta genererar en yarn.lock bredvid bun.lockb

Mitt tillvägagångssätt: jag committar bun.lockb till repot och genererar även en yarn.lock eller package-lock.json som en läsbar reserv. Hängslen och livrem.

Workspace-stöd#

Bun stöder npm/yarn-liknande workspaces:

json
{
  "name": "my-monorepo",
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}
bash
# Installera beroenden för alla workspaces
bun install
 
# Kör ett skript i en specifik workspace
bun run --filter packages/shared build
 
# Lägg till ett beroende i en specifik workspace
bun add react --filter apps/web

Workspace-stödet är stabilt och har förbättrats avsevärt. Den huvudsakliga luckan jämfört med pnpm är att Buns hantering av workspace-beroenden är mindre strikt — pnpms strikthet är en funktion för monorepos eftersom den fångar fantomberoendan.

Kompatibilitet med befintliga projekt#

Du kan lägga in bun install i nästan alla befintliga Node.js-projekt. Det läser package.json, respekterar .npmrc för registerkonfiguration och hanterar peerDependencies korrekt. Övergången är vanligtvis:

bash
# Steg 1: Radera befintlig lockfil och node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
 
# Steg 2: Installera med Bun
bun install
 
# Steg 3: Verifiera att din app fortfarande fungerar
bun run dev
# eller: node dist/server.js  (Bun pakethanterare, Node runtime)

Jag har gjort detta på ett dussin projekt och haft noll problem med själva pakethanteraren. Den enda fallgropen är om din CI-pipeline specifikt letar efter package-lock.json — du behöver uppdatera den för att hantera bun.lockb.

Node.js-kompatibilitet#

Det här är avsnittet där jag måste vara mest försiktig, eftersom situationen förändras varje månad. Per tidigt 2026, här är den ärliga bilden.

Vad som fungerar#

Den stora majoriteten av npm-paket fungerar utan modifiering. Bun implementerar de flesta inbyggda Node.js-modulerna:

typescript
// Alla dessa fungerar som förväntat i 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";

Både CommonJS och ESM fungerar. require() och import kan samexistera. TypeScript körs utan något kompileringssteg — Bun strippar typer vid parsning.

Ramverk som fungerar:

  • Express — fungerar, inklusive middleware-ekosystemet
  • Fastify — fungerar
  • Hono — fungerar (och är utmärkt med Bun)
  • Next.js — fungerar med förbehåll (mer om det nedan)
  • Prisma — fungerar
  • Drizzle ORM — fungerar
  • Socket.io — fungerar

Vad som inte fungerar (eller har problem)#

Luckorna tenderar att falla i några kategorier:

Nativa tillägg (node-gyp): Om ett paket använder C++-tillägg kompilerade med node-gyp kanske det inte fungerar med Bun. Bun har sitt eget FFI-system och stöder många nativa moduler, men täckningen är inte 100%. Till exempel har bcrypt (den nativa) haft problem — använd bcryptjs istället.

bash
# Kontrollera om ett paket använder nativa tillägg
ls node_modules/your-package/binding.gyp  # Om denna finns är det nativt

Specifika Node.js-interna delar: Vissa paket når in i Node.js-interna delar som process.binding() eller använder V8-specifika API:er. Dessa fungerar inte i Bun eftersom det körs på JavaScriptCore.

typescript
// Detta fungerar INTE i Bun — V8-specifikt
const v8 = require("v8");
v8.serialize({ data: "test" });
 
// Detta FUNGERAR — använd Buns motsvarighet eller ett tvärplattformsangreppssätt
const encoded = new TextEncoder().encode(JSON.stringify({ data: "test" }));

Worker-trådar: Bun stöder Web Workers och node:worker_threads, men det finns kantfall. Vissa avancerade användningsmönster — särskilt kring SharedArrayBuffer och Atomics — kan bete sig annorlunda.

vm-modulen: node:vm har partiellt stöd. Om din kod eller ett beroende använder vm.createContext() extensivt (vissa mallmotorer gör det), testa noggrant.

Kompatibilitetsspåraren#

Bun underhåller en officiell kompatibilitetsspårare. Kontrollera den innan du binder dig till Bun för ett projekt:

bash
# Kör Buns inbyggda kompatibilitetskontroll på ditt projekt
bun --bun node_modules/.bin/your-tool
 
# Flaggan --bun tvingar Buns runtime även för node_modules-skript

Min rekommendation: anta inte kompatibilitet. Kör din testsvit under Bun innan du bestämmer dig. Det tar fem minuter och sparar timmar av felsökning.

bash
# Snabb kompatibilitetskontroll — kör din fullständiga testsvit under Bun
bun test  # Om du använder bun test runner
# eller
bun run vitest  # Om du använder vitest

Buns inbyggda API:er#

Det är här Bun blir intressant. Istället för att bara återimplementera Node.js API:er erbjuder Bun sina egna API:er som är designade för att vara enklare och snabbare.

Bun.serve() — Den inbyggda HTTP-servern#

Det här är API:et jag använder mest. Det är rent, snabbt, och WebSocket-stöd är inbyggt.

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

Några saker att notera:

  1. Webbstandard Request/Response — inget proprietärt API. fetch-hanteraren tar emot en standard Request och returnerar en standard Response. Om du har skrivit en Cloudflare Worker känns detta identiskt.
  2. Response.json() — inbyggd JSON-responshjälpare.
  3. Ingen import behövsBun.serve är en global. Ingen require("http").

Här är ett mer realistiskt exempel med routing, JSON-body-parsning och felhantering:

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

Det är ett fullständigt CRUD-API med SQLite på ungefär 50 rader. Ingen Express, ingen ORM, ingen middleware-kedja. För små API:er och interna verktyg är detta min standarduppsättning nu.

Bun.file() och Bun.write() — Fil-I/O#

Buns fil-API är uppfriskande enkelt jämfört med fs.readFile():

typescript
// Läsa filer
const file = Bun.file("./config.json");
const text = await file.text();       // Läs som sträng
const json = await file.json();       // Parsa som JSON direkt
const bytes = await file.arrayBuffer(); // Läs som ArrayBuffer
const stream = file.stream();          // Läs som ReadableStream
 
// Filmetadata
console.log(file.size);  // Storlek i bytes
console.log(file.type);  // MIME-typ (t.ex. "application/json")
 
// Skriva filer
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"));
 
// Skriv en Response-body till en fil
const response = await fetch("https://example.com/data.json");
await Bun.write("./downloaded.json", response);

Bun.file()-API:et är lat — det läser inte filen förrän du anropar .text(), .json() osv. Det betyder att du kan skicka runt Bun.file()-referenser utan att belasta I/O förrän du faktiskt behöver datan.

Inbyggt WebSocket-stöd#

WebSockets är förstklassiga i 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) {
      // Sända till alla prenumeranter
      server.publish("chat", `${ws.data.userId}: ${message}`);
    },
 
    close(ws) {
      console.log(`Client disconnected: ${ws.data.userId}`);
      ws.unsubscribe("chat");
    },
  },
});

Mönstret server.publish() och ws.subscribe() är inbyggd pub/sub. Ingen Redis, inget separat WebSocket-bibliotek. För enkla realtidsfunktioner är detta otroligt bekvämt.

Inbyggd SQLite med bun:sqlite#

Det här överraskade mig mest. Bun levereras med SQLite inbyggt direkt i runtimen:

typescript
import { Database } from "bun:sqlite";
 
// Öppna eller skapa en databas
const db = new Database("myapp.db");
 
// WAL-läge för bättre concurrent läsprestanda
db.exec("PRAGMA journal_mode = WAL");
 
// Skapa tabeller
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'))
  )
`);
 
// Preparerade satser (återanvändbara, snabbare för upprepade frågor)
const insertUser = db.prepare(
  "INSERT INTO users (email, name) VALUES ($email, $name) RETURNING *"
);
 
const findByEmail = db.prepare(
  "SELECT * FROM users WHERE email = $email"
);
 
// Användning
const user = insertUser.get({
  $email: "alice@example.com",
  $name: "Alice",
});
console.log(user); // { id: 1, email: "alice@example.com", name: "Alice", ... }
 
// Transaktioner
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`);

Det här är synkron SQLite med prestandan hos ett C-bibliotek (för det är ett — Bun bäddar in libsqlite3 direkt). För CLI-verktyg, lokal-först-appar och små tjänster innebär inbyggd SQLite noll externa beroenden för ditt datalager.

Buns testkörare#

bun test är en drop-in-ersättning för Jest i de flesta fall. Den använder samma describe/it/expect-API och stöder de flesta Jest-matchers.

Grundläggande användning#

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
# Kör alla tester
bun test
 
# Kör specifik fil
bun test math.test.ts
 
# Kör tester som matchar ett mönster
bun test --test-name-pattern "adds numbers"
 
# Watch-läge
bun test --watch
 
# Täckning
bun test --coverage

Mockning#

Bun stöder Jest-kompatibel mockning:

typescript
import { describe, it, expect, mock, spyOn } from "bun:test";
import { fetchUsers } from "./api";
 
// Mocka en modul
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");
  });
});
 
// Spionera på en objektmetod
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 mot Vitest — Min ärliga jämförelse#

Jag använder Vitest för det här projektet (och de flesta av mina projekt). Här är varför jag inte har bytt helt:

Där bun test vinner:

  • Starthastighet. bun test börjar köra tester snabbare än Vitest hinner ladda klart sin konfiguration.
  • Noll konfiguration. Ingen vitest.config.ts behövs för grundläggande uppställningar.
  • Inbyggd TypeScript. Inget transformeringssteg.

Där Vitest fortfarande vinner:

  • Ekosystem: Vitest har fler plugins, bättre IDE-integration och en större community.
  • Konfiguration: Vitests konfigurationssystem är mer flexibelt. Anpassade reporters, komplexa uppsättningsfiler, flera testmiljöer.
  • Webbläsarläge: Vitest kan köra tester i en riktig webbläsare. Bun kan inte.
  • Kompatibilitet: Vissa testbibliotek (Testing Library, MSW) har testats mer grundligt med Vitest/Jest.
  • Snapshot-testning: Båda stöder det, men Vitests implementation är mer mogen med bättre diff-utmatning.

För ett nytt projekt med enkla testbehov skulle jag använda bun test. För ett etablerat projekt med Testing Library, MSW och komplex mockning behåller jag Vitest.

Buns bundler#

bun build är en snabb JavaScript/TypeScript-bundler. Den är inte en webpack-ersättning — den tillhör mer esbuild-kategorin: snabb, åsiktsfull och fokuserad på de vanliga fallen.

Grundläggande bundling#

bash
# Bundla en enda ingångspunkt
bun build ./src/index.ts --outdir ./dist
 
# Bundla för olika mål
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
 
# Minifiera
bun build ./src/index.ts --outdir ./dist --minify
 
# Generera sourcemaps
bun build ./src/index.ts --outdir ./dist --sourcemap external

Programmatiskt 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,    // Koddelning
  sourcemap: "external",
  external: ["react", "react-dom"],  // Bundla inte dessa
  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 stöder tree-shaking för 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
# Funktionen `unused` kommer inte att synas i utdatan

Där Bun Build brister#

  • Ingen CSS-bundling — Du behöver ett separat verktyg för CSS (PostCSS, Lightning CSS, Tailwind CLI).
  • Ingen HTML-generering — Den bundlar JavaScript/TypeScript, inte fullständiga webbappar.
  • Plugin-ekosystem — esbuild har ett mycket större plugin-ekosystem. Buns plugin-API är kompatibelt men communityn är mindre.
  • Avancerad koddelning — Webpack och Rollup erbjuder fortfarande mer sofistikerade chunk-strategier.

För att bygga ett bibliotek eller en enkel webbapps JS-bundle är bun build utmärkt. För komplexa appbyggen med CSS-moduler, bildoptimering och anpassade chunk-strategier behöver du fortfarande en fullständig bundler.

Bun Macros#

En genuint unik funktion: kompileringstidsexekvering via makron.

typescript
// build-info.ts — denna fil körs vid BYGGTID, inte 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() körs vid bundletid
// Resultatet infogas som ett statiskt värde
const info = getBuildInfo();
console.log(`Built at ${info.builtAt}, commit ${info.gitSha}`);

Efter bundling ersätts getBuildInfo() med det literala objektet — inget funktionsanrop vid runtime, ingen import av child_process. Koden kördes under bygget och resultatet infogades. Detta är kraftfullt för att bädda in byggmetadata, funktionsflaggor eller miljöspecifik konfiguration.

Att använda Bun med Next.js#

Det här är frågan jag får oftast, så låt mig vara mycket specifik.

Vad som fungerar idag#

Bun som pakethanterare för Next.js — fungerar perfekt:

bash
# Använd Bun för att installera beroenden, sedan Node.js för att köra Next.js
bun install
bun run dev    # Detta kör faktiskt "dev"-skriptet via Node.js som standard
bun run build
bun run start

Det här är vad jag gör för varje Next.js-projekt. Kommandot bun run <script> läser scripts-sektionen i package.json och kör den. Som standard använder det systemets Node.js för den faktiska exekveringen. Du får Buns snabba paketinstallation utan att ändra din runtime.

Bun-runtime för Next.js-utveckling:

bash
# Tvinga Next.js att köra under Buns runtime
bun --bun run dev

Det här fungerar för utveckling i de flesta fall. Flaggan --bun berättar för Bun att använda sin egen runtime istället för att delegera till Node.js. Hot module replacement fungerar. API-rutter fungerar. Serverkomponenter fungerar.

Vad som fortfarande är experimentellt#

Bun-runtime för Next.js produktionsbyggen:

bash
# Bygg med Bun-runtime
bun --bun run build
 
# Starta produktionsservern med Bun-runtime
bun --bun run start

Det här fungerar för många projekt men jag har stött på kantfall:

  1. Visst middleware-beteende skiljer sig — Om du använder Next.js-middleware som beror på Node.js-specifika API:er kan du stöta på kompatibilitetsproblem.
  2. Bildoptimering — Next.js bildoptimeringspipeline använder sharp, som är ett nativt tillägg. Det fungerar med Bun, men jag har sett enstaka problem.
  3. ISR (Incremental Static Regeneration) — Fungerar, men jag har inte stressttestat det under Bun i produktion.

Min rekommendation för Next.js#

Använd Bun som pakethanterare. Använd Node.js som runtime. Det ger dig hastighetsfördelarna med bun install utan någon kompatibilitetsrisk.

json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  }
}
bash
# Dagligt arbetsflöde
bun install      # Snabb paketinstallation
bun run dev      # Kör "next dev" via Node.js
bun run build    # Kör "next build" via Node.js

När Buns Node.js-kompatibilitet når 100% för Next.js interna användning (det är nära, men inte där ännu), byter jag. Tills dess sparar pakethanteraren ensam tillräckligt med tid för att motivera installationen.

Docker med Bun#

Den officiella Bun Docker-avbildningen är välunderhållen och produktionsklar.

Grundläggande Dockerfile#

dockerfile
FROM oven/bun:1 AS base
WORKDIR /app
 
# Installera beroenden
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
 
# Bygg (om det behövs)
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
 
# Kör inte som 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"]

Flerstegbygge för minimal avbildning#

dockerfile
# Byggsteg: fullständig Bun-avbildning med alla beroenden
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-steg: mindre basavbildning
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"]

Kompilering till en enda binärfil#

Det här är en av Buns killer features för driftsättning:

bash
# Kompilera din app till en enda körbar fil
bun build --compile ./src/server.ts --outfile server
 
# Utdatan är en fristående binärfil — varken Bun eller Node.js behövs för att köra den
./server
dockerfile
# Ultraminimal Docker-avbildning med kompilerad binärfil
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
 
# Slutgiltig avbildning — bara binärfilen
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"]

Den kompilerade binärfilen är vanligtvis 50-90 MB (den inkluderar Bun-runtimen). Det är större än en Go-binärfil men mycket mindre än en fullständig Node.js-installation plus node_modules. För containeriserade driftsättningar är den fristående naturen en betydande förenkling.

Storleksjämförelse#

bash
# Node.js-avbildning
docker images | grep node
# node:20-slim    ~180MB
 
# Bun-avbildning
docker images | grep bun
# oven/bun:1-slim  ~130MB
 
# Kompilerad binärfil på debian:bookworm-slim
# ~80MB bas + ~70MB binärfil = ~150MB totalt
 
# jämfört med Alpine med Node.js
# node:20-alpine   ~130MB + node_modules

Binärfilsansatsen eliminerar node_modules helt från den slutgiltiga avbildningen. Ingen npm install i produktion. Ingen leveranskedjeyta från hundratals paket. Bara en fil.

Migrationsmönster#

Om du överväger att gå över till Bun, här är den inkrementella vägen jag rekommenderar:

Fas 1: Enbart pakethanterare (Noll risk)#

bash
# Ersätt npm/yarn/pnpm med bun install
# Ändra din CI-pipeline:
# Före:
npm ci
 
# Efter:
bun install --frozen-lockfile

Inga kodändringar. Inga runtime-ändringar. Bara snabbare installationer. Om något går sönder (det kommer det inte), återställ genom att radera bun.lockb och köra npm install.

Fas 2: Skript och verktyg#

bash
# Använd bun för att köra utvecklingsskript
bun run dev
bun run lint
bun run format
 
# Använd bun för engångsskript
bun run scripts/seed-database.ts
bun run scripts/migrate.ts

Använder fortfarande Node.js som runtime för din faktiska applikation. Men skript drar nytta av Buns snabbare start och inbyggda TypeScript-stöd.

Fas 3: Testkörare (Medelhög risk)#

bash
# Ersätt vitest/jest med bun test för enkla testsviter
bun test
 
# Behåll vitest för komplexa testuppsättningar
# (Testing Library, MSW, anpassade miljöer)

Kör din fullständiga testsvit under bun test. Om allt passerar har du eliminerat ett devDependency. Om vissa tester misslyckas på grund av kompatibilitet, behåll Vitest för dem och använd bun test för resten.

Fas 4: Runtime för nya tjänster (Beräknad risk)#

typescript
// Nya mikrotjänster eller API:er — börja med Bun från dag ett
Bun.serve({
  port: 3000,
  fetch(req) {
    // Din nya tjänst här
  },
});

Migrera inte befintliga Node.js-tjänster till Bun-runtime. Skriv istället nya tjänster med Bun från start. Det begränsar din sprängningsradie.

Fas 5: Runtime-migrering (Avancerat)#

bash
# Först efter grundlig testning:
# Ersätt node med bun för befintliga tjänster
# Före:
node dist/server.js
 
# Efter:
bun dist/server.js

Jag rekommenderar detta bara för tjänster med utmärkt testtäckning. Kör dina lasttester under Bun innan du byter produktion.

Miljövariabler och konfiguration#

Bun hanterar .env-filer automatiskt — inget dotenv-paket behövs:

bash
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
API_KEY=sk-test-12345
PORT=3000
typescript
// Dessa är tillgängliga utan någon import
console.log(process.env.DATABASE_URL);
console.log(process.env.API_KEY);
console.log(Bun.env.PORT); // Bun-specifikt alternativ

Bun laddar .env, .env.local, .env.production osv. automatiskt, enligt samma konvention som Next.js. Ett beroende mindre i din package.json.

Felhantering och felsökning#

Buns felutmatning har förbättrats avsevärt, men den är fortfarande inte lika polerad som Node.js i vissa fall:

bash
# Buns debugger — fungerar med VS Code
bun --inspect run server.ts
 
# Buns inspect-brk — pausa på första raden
bun --inspect-brk run server.ts

För VS Code, lägg till detta i din .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
    }
  ]
}

Stackspår i Bun är generellt korrekta och inkluderar source maps för TypeScript. Den främsta felsökningsluckan är att vissa Node.js-specifika felsökningsverktyg (som ndb eller clinic.js) inte fungerar med Bun.

Säkerhetsöverväganden#

Några saker att tänka på om du utvärderar Bun för produktion:

Mognad: Node.js har varit i produktion i 15+ år. Varje kantfall i HTTP-parsning, TLS-hantering och strömbearbetning har hittats och fixats. Bun är yngre. Det är väl testat, men ytan för oupptäckta buggar är större.

Säkerhetsuppdateringar: Bun-teamet levererar uppdateringar ofta, men Node.js säkerhetsteam har en formell CVE-process, koordinerad avslöjning och en längre meritlista. För säkerhetskritiska applikationer spelar detta roll.

Leveranskedja: Buns inbyggda funktioner (SQLite, HTTP-server, WebSockets) innebär färre npm-beroenden. Färre beroenden innebär en mindre attackyta för leveranskedjan. Det här är en genuin säkerhetsfördel.

bash
# Jämför antal beroenden
# Ett typiskt Express + SQLite + WebSocket-projekt:
npm ls --all | wc -l
# ~340 paket
 
# Samma funktionalitet med Buns inbyggda funktioner:
bun pm ls --all | wc -l
# ~12 paket (bara din applikationskod)

Det är en meningsfull minskning av antalet paket du litar på med din produktionsbelastning.

Prestandajustering#

Några Bun-specifika prestandatips:

typescript
// Använd Bun.serve()-alternativ för produktionsjustering
Bun.serve({
  port: 3000,
 
  // Öka maximal storlek för request body (standard är 128MB)
  maxRequestBodySize: 1024 * 1024 * 50, // 50MB
 
  // Aktivera utvecklingsläge för bättre felsidor
  development: process.env.NODE_ENV !== "production",
 
  // Återanvänd port (användbart för nollavbrottsomstarter)
  reusePort: true,
 
  fetch(req) {
    return new Response("OK");
  },
});
typescript
// Använd Bun.Transpiler för runtime-kodtransformering
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  target: "browser",
});
 
const code = transpiler.transformSync(`
  const App: React.FC = () => <div>Hello</div>;
  export default App;
`);
bash
# Buns minnesanvändningsflaggor
bun --smol run server.ts  # Minska minnesavtrycket (något långsammare)
 
# Ange maximal heapstorlek
BUN_JSC_forceRAMSize=512000000 bun run server.ts  # ~512MB gräns

Vanliga fallgropar#

Efter ett år av att använda Bun, här är sakerna som har snubblat mig:

1. Globalt Fetch-beteende skiljer sig#

typescript
// Node.js 18+ fetch och Buns fetch är något olika
// i hur de hanterar vissa headers och omdirigeringar
 
// Bun följer omdirigeringar som standard (som webbläsare)
// Node.js fetch följer också omdirigeringar, men beteendet
// med vissa statuskoder (303, 307, 308) kan skilja sig
 
const response = await fetch("https://api.example.com/data", {
  redirect: "manual", // Var explicit om omdirigeringshantering
});

2. Processavslutningsbeteende#

typescript
// Bun avslutas när händelseslingan är tom
// Node.js fortsätter ibland köra på grund av kvarvarande handtag
 
// Om ditt Bun-skript avslutas oväntat håller inget
// händelseslingan vid liv
 
// Detta avslutas omedelbart i Bun:
setTimeout(() => {}, 0);
 
// Detta fortsätter köra:
setTimeout(() => {}, 1000);
// (Bun avslutas efter att timeouten har utlösts)

3. TypeScript-konfiguration#

typescript
// Bun har sina egna tsconfig-standardvärden
// Om du delar ett projekt mellan Bun och Node.js,
// var explicit i din tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"]  // Lägg till Buns typdefinitioner
  }
}
bash
# Installera Bun-typer
bun add -d @types/bun

4. Hot Reload under utveckling#

bash
# Bun har inbyggt watch-läge
bun --watch run server.ts
 
# Detta startar om processen vid filändringar
# Det är inte HMR (Hot Module Replacement) — det är en fullständig omstart
# Men eftersom Bun startar så snabbt känns det omedelbart

5. Konfigurationsfilen bunfig.toml#

toml
# bunfig.toml — Buns konfigurationsfil (valfri)
 
[install]
# Använd ett privat register
registry = "https://npm.mycompany.com"
 
# Scopade register
[install.scopes]
"@mycompany" = "https://npm.mycompany.com"
 
[test]
# Testkonfiguration
coverage = true
coverageReporter = ["text", "lcov"]
 
[run]
# Shell att använda för bun run
shell = "bash"

Min bedömning#

Efter ett års produktionsanvändning, här är var jag har landat:

Där jag använder Bun idag#

Pakethanterare för alla projekt — inklusive den här Next.js-bloggen. bun install är snabbare, och kompatibiliteten är i princip perfekt. Jag ser ingen anledning att använda npm eller yarn längre. pnpm är det enda alternativet jag skulle överväga (för dess strikta beroendehantering i monorepos).

Runtime för skript och CLI-verktyg — Varje TypeScript-fil jag behöver köra en gång kör jag med bun. Inget kompileringssteg. Snabb start. Inbyggd .env-laddning. Det har ersatt ts-node och tsx i mitt arbetsflöde helt.

Runtime för små API:er och interna verktygBun.serve() + bun:sqlite är en otroligt produktiv stack för interna verktyg, webhook-hanterare och små tjänster. Driftsättningsmodellen "en binärfil, inga beroenden" är övertygande.

Testkörare för enkla projekt — För projekt med enkla testbehov är bun test snabbt och kräver noll konfiguration.

Där jag håller mig till Node.js#

Produktion med Next.js — Inte för att Bun inte fungerar, utan för att risk-belöningskalkylen inte motiverar det ännu. Next.js är ett komplext ramverk med många integrationspunkter. Jag vill ha den mest stridstestade runtimen under det.

Kritiska produktionstjänster — Mina huvudsakliga API-servrar kör Node.js bakom PM2. Övervakningsekosystemet, felsökningsverktygen, den operativa kunskapen — allt är Node.js. Bun kommer dit, men det är inte där ännu.

Allt med nativa tillägg — Om en beroendekedja inkluderar C++ nativa tillägg provar jag inte ens Bun. Det är inte värt att felsöka kompatibilitetsproblemen.

Team som inte är bekanta med Bun — Att introducera Bun som runtime till ett team som aldrig har använt det lägger till kognitiv overhead. Som pakethanterare, okej. Som runtime, vänta tills teamet är redo.

Vad jag bevakar#

Buns kompatibilitetsspårare — När den når 100% för de Node.js API:er jag bryr mig om omvärderar jag.

Ramverksstöd — Next.js, Remix och SvelteKit har alla varierande nivåer av Bun-stöd. När ett av dem officiellt stöder Bun som produktionsruntime är det en signal.

Företagsadoption — När företag med riktiga SLA:er kör Bun i produktion och skriver om det är mognadsfrågan besvarad.

1.2+-releaseserien — Bun rör sig snabbt. Funktioner landar varje vecka. Den Bun jag använder idag är meningsfullt bättre än den Bun jag provade för ett år sedan.

Avslutning#

Bun är ingen universallösning. Det gör inte en långsam app snabb och det gör inte ett dåligt designat API väldesignat. Men det är en genuin förbättring av utvecklarupplevelsen för JavaScript-ekosystemet.

Det jag uppskattar mest med Bun är inte någon enskild funktion. Det är minskningen av verktygskedjans komplexitet. En binärfil som installerar paket, kör TypeScript, bundlar kod och kör tester. Ingen tsconfig.json för skript. Ingen Babel. Ingen separat testkörarkonfiguration. Bara bun run your-file.ts och det fungerar.

Det praktiska rådet: börja med bun install. Det är noll risk, omedelbar nytta. Prova sedan bun run för skript. Utvärdera sedan resten baserat på dina specifika behov. Du behöver inte gå all-in. Bun fungerar utmärkt som en partiell ersättning, och det är förmodligen så de flesta borde använda det idag.

JavaScript-runtimelandskapet är bättre med Bun i det. Konkurrens gör Node.js bättre också — Node.js 22+ har blivit betydligt snabbare, delvis som svar på Buns press. Alla vinner.

Relaterade inlägg