Zum Inhalt springen
CASOON

Bun als Bundler: Plugins und Tree-Shaking

Build-Prozesse optimieren ohne Webpack oder Rollup

6 Minuten
Bun als Bundler: Plugins und Tree-Shaking
#Bun #Bundler #JavaScript #TypeScript
SerieBun: Die moderne JavaScript-Runtime
Teil 3 von 5

Bun ist nicht nur Runtime, sondern auch Bundler. Kein Webpack, kein Rollup, keine separate Konfigurationsdatei. Der Bundler ist in die Runtime integriert und nutzt dieselbe schnelle Zig-basierte Architektur.

Grundlegende Nutzung

Ein einfacher Build:

bun build ./src/index.ts --outdir ./dist

Oder programmatisch:

const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
});

if (!result.success) {
  console.error("Build failed:", result.logs);
}

Das Ergebnis ist ein optimiertes JavaScript-Bundle.

Build-Optionen

Die wichtigsten Konfigurationsoptionen:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  
  // Zielumgebung
  target: "browser", // oder "bun", "node"
  
  // Optimierungen
  minify: true,
  sourcemap: "external",
  
  // Splitting für Code-Splitting
  splitting: true,
  
  // Externe Pakete nicht bundlen
  external: ["react", "react-dom"],
  
  // Umgebungsvariablen inlinen
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
});

Plugins entwickeln

Plugins erweitern den Build-Prozess. Die API ist einfach:

import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "my-plugin",
  setup(build) {
    // Hooks registrieren
  },
};

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  plugins: [myPlugin],
});

onLoad Hook

Der häufigste Hook transformiert Dateien:

const textPlugin: BunPlugin = {
  name: "text-loader",
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const content = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(content)}`,
        loader: "js",
      };
    });
  },
};

Jetzt können .txt-Dateien importiert werden:

import readme from "./README.txt";
console.log(readme);

onResolve Hook

Für Custom Module Resolution:

const aliasPlugin: BunPlugin = {
  name: "alias",
  setup(build) {
    build.onResolve({ filter: /^@components\// }, (args) => {
      const path = args.path.replace("@components/", "./src/components/");
      return { path };
    });
  },
};

Praktisches Beispiel: YAML-Loader

import { parse } from "yaml";

const yamlPlugin: BunPlugin = {
  name: "yaml-loader",
  setup(build) {
    build.onLoad({ filter: /\.ya?ml$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      const data = parse(text);
      return {
        contents: `export default ${JSON.stringify(data)}`,
        loader: "js",
      };
    });
  },
};

Tree-Shaking

Tree-Shaking ist standardmäßig aktiviert. Der Bundler entfernt ungenutzte Exports automatisch.

ESM-Module

Bei ES-Modulen funktioniert Tree-Shaking optimal:

// utils.ts
export const used = () => "wird genutzt";
export const unused = () => "wird entfernt";

// index.ts
import { used } from "./utils";
console.log(used());
// unused wird aus dem Bundle entfernt

CommonJS-Einschränkungen

Bei CommonJS ist statische Analyse schwieriger:

// Problematisch - dynamischer Export
module.exports = require("./dynamic");

// Besser - statischer Export
module.exports = {
  foo: require("./foo"),
  bar: require("./bar"),
};

Bun konvertiert CommonJS zu ESM wo möglich, aber dynamische Patterns verhindern Tree-Shaking.

Seiteneffekte markieren

Für optimales Tree-Shaking in package.json:

{
  "sideEffects": false
}

Oder spezifische Dateien mit Seiteneffekten:

{
  "sideEffects": ["./src/polyfills.js", "*.css"]
}

Code-Splitting

Mit splitting: true erzeugt Bun separate Chunks:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  splitting: true,
});

Dynamische Imports werden automatisch in separate Dateien extrahiert:

// Wird als separater Chunk gebundelt
const module = await import("./heavy-module");

Sourcemaps

Für Debugging in Production:

await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
  sourcemap: "external", // oder "inline", "none"
  minify: true,
});

Externe Sourcemaps landen als .js.map-Dateien im Output-Verzeichnis.

Build-Artefakte

Bun.build gibt ein Array von BuildArtifact-Objekten zurück:

const result = await Bun.build({
  entrypoints: ["./src/index.ts"],
  outdir: "./dist",
});

for (const artifact of result.outputs) {
  console.log(artifact.path);      // Ausgabepfad
  console.log(artifact.kind);      // "entry-point", "chunk", "asset"
  console.log(artifact.loader);    // "js", "css", etc.
  
  // Inhalt lesen
  const text = await artifact.text();
}

Vergleich mit anderen Bundlern

FeatureBunWebpackRollupesbuild
Zero-ConfigJaNeinNeinJa
TypeScriptNativeLoaderPluginNative
Tree-ShakingJaJaJaJa
Code-SplittingJaJaJaJa
Plugin-APIEinfachKomplexMittelEinfach
SpeedSehr schnellLangsamMittelSehr schnell

Bun und esbuild sind ähnlich schnell. Buns Vorteil: Es ist gleichzeitig Runtime, Bundler und Package Manager.


Quellen

Realistische Performance-Vergleichswerte

Aus eigenen Build-Tests (Stand 2026):

BundlerMittlerer Projekt-Build (50 Module)Großer Build (500+ Module)
Bun Bundler0,5–2 s5–15 s
esbuild1–3 s8–20 s
Vite (esbuild + Rollup)2–5 s15–40 s
Webpack 58–25 s60–180 s
Rollup3–8 s30–90 s

Bei einfachen Builds sind die Unterschiede oft praktisch egal — bei großen Monorepos wird Bun zur klaren Wahl.

Wann Bun als Bundler nicht passt

  • Bei reifer Vite/Webpack-Toolchain mit vielen Custom-Plugins: Bun’s Plugin-API ist solide, aber das Ökosystem ist deutlich kleiner. Wer Webpack-spezifische Loader (für Marko, Pug, Vue 2 etc.) braucht, sollte beim alten Stack bleiben.
  • Bei Server Components / SSR: Bun’s Bundler hat Server-Component-Support entwickelt, aber Next.js und Vite haben hier mehr Reife. Bei produktiven SSR-Setups Vite/Next bevorzugen.
  • Bei sehr großen Enterprise-Apps mit komplexem Build: Hier ist die Tooling-Auswahl wichtiger als reine Geschwindigkeit. Webpack mit etablierten Patterns bleibt sicher.

Was Tree-Shaking in der Praxis bringt

  • Typische Bundle-Reduktion: 15–40 % gegenüber unoptimiertem Build, je nach Library-Mix. Lodash, date-fns, React-Icons profitieren stark.
  • Voraussetzungen: ES-Modules statt CommonJS, "sideEffects": false in package.json für Library-Pakete, keine dynamischen Imports mit String-Concatenation.
  • Typische Fallstricke: CSS-Imports werden oft als Side-Effects markiert und nicht entfernt. Wer Tailwind nutzt, sollte die content-Konfiguration präzise halten, damit unverwendetes CSS rausgefiltert wird.

Wann Tree-Shaking nicht den erwarteten Effekt bringt

  • Bei Libraries mit globalen Polyfills: Pakete, die Window-Properties oder Prototype-Erweiterungen verändern, können nicht getreeshakt werden.
  • Bei Dynamic Imports mit Variablen: import('./locales/' + locale + '.js') verhindert statische Analyse — der Bundler muss konservativ alles einschließen.
  • Bei TypeScript-Decoratoren oder Reflect-Metadaten: Manche Frameworks (NestJS, TypeORM) brauchen Runtime-Reflection, die Tree-Shaking verhindert.