Build-Prozesse optimieren ohne Webpack oder Rollup
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
| Feature | Bun | Webpack | Rollup | esbuild |
|---|---|---|---|---|
| Zero-Config | Ja | Nein | Nein | Ja |
| TypeScript | Native | Loader | Plugin | Native |
| Tree-Shaking | Ja | Ja | Ja | Ja |
| Code-Splitting | Ja | Ja | Ja | Ja |
| Plugin-API | Einfach | Komplex | Mittel | Einfach |
| Speed | Sehr schnell | Langsam | Mittel | Sehr 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):
| Bundler | Mittlerer Projekt-Build (50 Module) | Großer Build (500+ Module) |
|---|---|---|
| Bun Bundler | 0,5–2 s | 5–15 s |
| esbuild | 1–3 s | 8–20 s |
| Vite (esbuild + Rollup) | 2–5 s | 15–40 s |
| Webpack 5 | 8–25 s | 60–180 s |
| Rollup | 3–8 s | 30–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": falsein 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.