JavaScript-Entwicklung auf Turbo: Ein Blick auf Bun und seine Innovationen

24.01.2024Ricky Elfner
Mobile JavaScript Frontend Framework Hands-on How-to

banner

Diese Woche beschäftigen wir uns mit Bun, welches im September 2023 in der Version 1.0 released wurde. Die Entwickler beschreiben Bun als ein All-in-one Toolkit für JavaScript und TypeScript Anwendungen.

Die Bun Runtime

Im Herzen der Bun-Runtime wurde eine schnelle JavaScript-Runtime implementiert, welche dabei als problemlose Alternative zu Node.js konzipiert wurde. Sie ist in Zig geschrieben und wird von JavaScriptCore unter der Haube angetrieben, was die Startzeiten und den Speicherverbrauch dramatisch reduziert. Zig ist eine moderne, leistungsstarke Programmiersprache die durch die Kombination aus Stabilität, Effizienz und direkter Kontrolle über Ressourcen ausgewählt wurde.

Das Bun CLI-Tool

Das Command-Line-Tool von Bun ist zusätzlich mit einem Test-Runner, Script Runner und Node.js kompatiblem Paketmanager ausgestattet. Der grosse Unterschied dabei ist die deutlich bessere Performance im Vergleich zu bestehenden Tools. Ein weiterer grosser Vorteil liegt darin, dass bun ohne grössere Änderung in bestehenden Node.js-Anwendungen verwendet werden kann.

Schnellere Developer Experience als Node.js

Seit der Einführung von Node.js vor etwa 14 Jahren kamen immer wieder unzählige neue Features und Tools dazu, wodurch Node.js natürlich extrem wuchs. Dies führte zu einer gewissen Komplexität und Grösse, die zu Einbussen bei der Performance führte. Die vorhandenen Tools sind in den meisten Fällen sehr gut, da jedoch alle auf einmal vorhanden sind, führt dies zu einer langsamen Developer-Experience.

Dies hat vor allem den Grund, dass die vorhanden Tools oft Redundanzen in ihren Aufgaben führen. Führt man beispielsweise jest aus, wird der Code mindestens dreimal geparsed. Dies ist genau die Stelle an der bun ansetzen und auch durch seine Geschwindigkeit überzeugen möchte, ohne dabei die Vorzüge von JavaScript zu verlieren.

Bun’s JavaScript Runtime

Bei der Einwicklung wurde Wert vor allem auf Geschwindigkeit gelegt. Vor allem bei TypeScript-Files ist alleine der Startprozess von Bun 4x so schnell wie von Node.js.

img

Bild Quelle: bun.sh

Wieso ist Bun so viel schneller?

Dieser Unterschied ist vor allem Möglich, da hier nicht Google’s V8 Engine benutzt wird, sondern Apples WebKit Engine.

Doch auch die Unterstützung von Typescript und JSX-Dateien war wichtig. Dies wird so gelöst, dass Buns Transpiler diese Dateien vor der Ausführung in herkömmliches JavaScript umwandelt, ohne zusätzliche Dependencies:

1
2
3
bun index.ts
bun index.jsx
bun index.tsx

Weiterer Support ist natürlich auch für ES-Modules gegeben und empfohlen. Sollte dennoch wie bei Millionen von Packages auf npm CommonJS benötigt werden, wird auch dies unterstützt.

Bun unterstützt import und require() in derselben Datei

In Bun besteht die bemerkenswerte Möglichkeit, sowohl import als auch require() innerhalb derselben Datei zu nutzen – eine Flexibilität, die in Node.js standardmäßig nicht gegeben ist, es sei denn, man verwendet spezielle Funktionen wie das “Mixed-Modules”-Feature.

Standard Web-API-Unterstützung

Bun implementiert Standard-Web-APIs wie fetch, Request, Response, WebSocket, and ReadableStream. Bun wird vom JavaScriptCore-Engine angetrieben, die von Apple für Safari entwickelt wurde. Daher verwenden einige APIs wie Headers und URL direkt die Implementierung von Safari.

Vollständige Kompatibilität mit Node.js Globals und Modules

Natürlich ist das Ziel von Bun die vollständige Kompatibilität mit den Node.js integrierten globals (process, Buffer) und modules (path, fs, http, usw.). Hier ist jedoch zu erwähnen dass dies ist ein laufenderProzess ist, der noch nicht vollständig abgeschlossen ist. Der aktuelle Stand kann hier geprüft werden: https://bun.sh/docs/runtime/nodejs-apis.

Ebenfalls soll laut Bun die Kompatibilität zu bestehenden Frameworks gegeben sein. Herzu gehören:

  • Next.js
  • Remix
  • Nuxt
  • Astro
  • SvelteKit
  • Nest
  • SolidStart
  • Vite

Hot-Reloading mit Bun

Bun vereinfacht die Entwicklungsarbeit erheblich. Du kannst Bun mit dem Parameter --hot ausführen, um das Hot-Reloading zu aktivieren, das deine Anwendung neu lädt, wenn Dateien geändert werden.

1
bun --hot server.ts

Im Gegensatz zu Werkzeugen wie nodemon, die den gesamten Prozess hart neu starten, lädt Bun deinen Code neu, ohne den alten Prozess zu beenden. Das bedeutet, dass HTTP- und WebSocket-Verbindungen nicht getrennt werden und der State nicht verloren geht. Dieser Ansatz stellt einen Vorteil gegenüber gängigen Node.js-Tools dar, die bei Änderungen im Code den gesamten Prozess neu starten und daher bestehende Verbindungen unterbrechen.

Extreme Flexibilität mit Bun Plugins

Bun ist darauf ausgerichtet, extrem anpassbar zu sein. Mit der Möglichkeit, Plugins zu definieren, kannst du Imports abfangen und individuelle Ladevorgänge durchführen. Ein Plugin kann beispielsweise die Unterstützung für weitere Dateitypen wie yaml oder png hinzufügen. Die Plugin-API ist von esbuild inspiriert, was bedeutet, dass die meisten esbuild-Plugins problemlos mit Bun verwendet werden können. Dies verleiht dir maximale Flexibilität bei der Gestaltung deiner Entwicklungsprozesse.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { plugin } from "bun";

plugin({
  name: "YAML",
  async setup(build) {
    const { load } = await import("js-yaml");
    const { readFileSync } = await import("fs");
    build.onLoad({ filter: /\.(yaml|yml)$/ }, (args) => {
      const text = readFileSync(args.path, "utf8");
      const exports = load(text) as Record<string, any>;
      return { exports, loader: "object" };
    });
  },
});

Bun APIs

Bun bringt top optimierte APIs als Standardbibliothek mit, die genau das bieten, was du als Entwickler am meisten brauchst. Im Gegensatz zu den Node.js APIs, die eher für Rückwärtskompatibilität gedacht sind, sind diese nativen Bun-APIs darauf ausgerichtet, schnell und intuitiv benutzbar zu sein.

Bun.file()

Bun geht noch einen Schritt weiter, indem es ein BunFile zurückgibt – eine Erweiterung des Web-Standard Files. Diese Datei ermöglicht das bedarfsgesteuerte, lazy laden von Inhalten in verschiedenen Formaten, was natürlich die Tür zu vielfältigen Anwendungsmöglichkeiten öffnet.

1
2
3
4
5
6
const file = Bun.file("package.json");

await file.text(); // string
await file.arrayBuffer(); // ArrayBuffer
await file.blob(); // Blob
await file.json(); // {...}

Bun.write()

Mit Bun vereinfacht die vielseitige Bun.write()-API das Schreiben verschiedenster Daten auf die Festplatte. Ob es sich um einen String, binäre Daten, Blobs oder sogar ein Response-Objekt handelt – diese einzelnen Methoden optimieren den Schreibvorgang. Diese Flexibilität verbessert die Entwicklererfahrung erheblich, da es nahtlos möglich ist, unterschiedliche Datentypen zu verarbeiten, ohne auf mehrere komplexe Funktionen zurückgreifen zu müssen.

1
2
3
4
await Bun.write("index.html", "<html/>");
await Bun.write("index.html", Buffer.from("<html/>"));
await Bun.write("index.html", Bun.file("home.html"));
await Bun.write("index.html", await fetch("https://example.com/"));

Bun.serve()

Bun bietet mit Bun.serve() die Möglichkeit, einen HTTP-Server, WebSocket-Server oder beides zu starten. Dabei nutzt es vertraute Web-Standard-APIs wie Request und Response. Beeindruckend ist, dass Bun bis zu 4 mal mehr Anfragen pro Sekunde bedienen kann als Node.js. Diese Leistungsstärke in Kombination mit bekannten APIs macht Bun zu einer effizienten Wahl für das Bereitstellen von Serveranwendungen, wobei es gleichzeitig eine nahtlose Integration in gängige Webstandards ermöglicht.

1
2
3
4
5
6
Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello from Bun!");
  },
});

Sogar TLS Konfigurationen können schnell und leicht implementiert werden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Hello from Bun!");
  },
  tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
  }
});

Bun macht die Unterstützung von WebSockets neben HTTP mühelos. Definiere einfach einen Eventhandler innerhalb von websocket. Dies steht im starken Kontrast zu Node.js, das keine integrierte WebSocket-API bereitstellt und auf externe Abhängigkeiten wie ws angewiesen ist. Mit Bun wird die Implementierung von WebSockets in deine Anwendung so einfach wie nativer Code, ohne den zusätzlichen Aufwand externer Bibliotheken. Dazu kann Bun bis zu 5 mal mehr Anfragen pro Sekunde bedienen als Node.js.

1
2
3
4
5
6
7
8
Bun.serve({
  fetch() { ... },
  websocket: {
    open(ws) { ... },
    message(ws, data) { ... },
    close(ws, code, reason) { ... },
  },
});

bun:sqlite

Bun bietet integrierte Unterstützung für SQLite mit einer API, die sich von better-sqlite3 inspirieren lässt, aber in nativem Code geschrieben ist, um die Geschwindigkeit zu maximieren. Im Vergleich zu better-sqlite3 auf Node.js ermöglicht Bun Abfragen an SQLite mit bis zu 4-facher Geschwindigkeit. Diese nativ implementierte Unterstützung gewährleistet nicht nur eine schnelle und effiziente Datenbankinteraktion, sondern zeigt auch Buns Engagement für optimierte Leistung und nahtlose Integration von Datenbankfunktionalitäten in den Entwicklungsprozess.

1
2
3
4
5
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Bun' as runtime;");
query.get(); // => { runtime: "Bun" }

Bun.password

Neben seinem vielseitigen Plugin-System erleichtert Bun auch die Umsetzung von gemeinsamen, jedoch komplexen Aufgaben, die Entwickler möglicherweise nicht von Grund auf selbst implementieren möchten.

Mit der Bun.password-API können Entwickler mühelos Passwörter hashen und überprüfen, und das ganz ohne externe Abhängigkeiten. Diese Funktion ermöglicht die Verwendung anerkannter kryptografischer Algorithmen wie bcrypt oder argon2. Indem Bun diese einsatzbereiten Lösungen für essenzielle aber komplexe Funktionen bietet, vereinfacht es den Entwicklungsprozess und stärkt bewährte Sicherheitspraktiken. Entwickler können sich somit auf den Aufbau robuster und sicherer Anwendungen konzentrieren.

1
2
3
4
5
6
const password = "super-secure-pa$$word";
const hash = await Bun.password.hash(password);
// => $argon2id$v=19$m=65536,t=2,p=1$tFq+9AVr1bfPxQdh...

const isMatch = await Bun.password.verify(password, hash);
// => true

Der Package Manager

Auch wenn Bun nicht als primäre Laufzeitumgebung verwendet wird, kann der integrierte Paketmanager von Bun erheblich dazu beitragen, die Effizienz deines Entwicklungsworkflows zu steigern. Schluss mit den langen Wartezeiten beim Installieren von Abhängigkeiten, die du vielleicht von anderen Paketmanagern kennst.

Bun ist um ein Vielfaches schneller als npm, yarn und pnpm. Dies wird durch die Nutzung eines globalen Modulcaches erreicht, um wiederholte Downloads aus dem npm-Registry zu vermeiden. Zudem nutzt es die schnellsten Systemaufrufe, die auf jedem Betriebssystem verfügbar sind.

Sieht man sich jedoch die Befehle an, erkennt man dabei eigentlich keinen Unterschied zu npm.

1
2
3
4
bun install
bun add <package> [--dev|--production|--peer]
bun remove <package>
bun update <package>

Auch beim Ausführen von Scripts mittels bun run spart man jedes mal etwa 150 Millisekunden

Script Runner ø Zeit
npm run 176ms
yarn run 131ms
pnpm run 259ms
bun run 7ms

Der Test Runner

Wenn du bereits Tests in JavaScript geschrieben hast, kennst du wahrscheinlich Jest, das die “expect"-Style-APIs eingeführt hat.

Bun geht mit seinem integrierten Testmodul bun:test einen Schritt weiter und ist vollständig kompatibel mit Jest. Du kannst deine Tests ganz einfach mit dem Befehl bun test ausführen und profitierst dabei von allen Vorteilen der Bun-Laufzeitumgebung, einschließlich umfassender Unterstützung für TypeScript und JSX.

1
2
3
4
5
import { test, expect } from "bun:test";

test("2 + 2", () => {
  expect(2 + 2).toBe(4);
});

Die Migration von Jest oder Vitest zu Bun gestaltet sich spielend einfach. Jegliche Importe von @jest/globals oder vitest werden automatisch auf bun:test umgestellt, sodass alles reibungslos funktioniert, selbst ohne Codeänderungen.

1
2
3
4
5
import { test } from "@jest/globals";

describe("test suite", () => {
  // ...
});

In einem Leistungsvergleich mit dem Testpaket für zod hat Bun wieder die Nase vorn. Es war satte 13 mal schneller als Jest und 8-mal schneller als Vitest. Diese Performancesteigerung ist nicht nur eine Zahl; sie bedeutet effizienteres und schnelleres Testen für deine Projekte.

Zod ist eine leistungsstarke TypeScript-Bibliothek für die Definition von Datenstrukturen und deren Validierung, die es Entwicklern ermöglicht, klare und sichere Schemata für ihre Anwendungen zu erstellen.

img

Bild Quelle: bun.sh

Buns Matcher (bspw. expect().toEqual() ) sind durch die Implementierung nativem Code 100-mal schneller als in Jest und 10-mal schneller als in Vitest.

Es gibt auch direkt eine Implementierung für GitHub Actions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: oven-sh/setup-bun@v1
      - run: bun test

Dadurch werden automatisch Annotation zu den Testfehlern hinzugefügt, damit deine Logs leicht verständlich sind.

Der Bundler

Bun hebt sich nicht nur als ein JavaScript- und TypeScript-Bundler sowie Minifier ab, sondern als eine kraftvolle Lösung, um deinen Code für den Browser, Node.js und andere Plattformen zu optimieren und zu bündeln. Inspiriert von esbuild bietet Bun eine durchdachte Plugin-API, die sowohl für das Bündeln als auch für die Laufzeitumgebung höchst effizient ist. Sogar das vorherige .yaml-Plugin ist weiter einsatzbereit, um .yaml-Dateien nahtlos während des Bündelns zu integrieren.

In puncto Geschwindigkeit hat Bun laut den Benchmarks von esbuild natürlich die Nase vorn. Es ist 1,75-mal schneller als esbuild selbst, 150-mal schneller als Parcel 2, 180-mal schneller als Rollup + Terser und beeindruckende 220-mal schneller als Webpack. Dank der integrierten Laufzeitumgebung und des Bundlers kann Bun Funktionen ausführen, die kein anderer Bundler beherrscht.

img

Bild Quelle: bun.sh

JavaScript Macros

Aber das ist noch nicht alles. Bun führt die Idee der JavaScript-Macros ein – das sind kleine Funktionen, die während des Bündelns deinen Code optimieren. Die Rückgabewerte dieser Funktionen werden direkt in dein Bundle eingefügt. Ein typischer Anwendungsfall wäre beispielsweise das Auslesen einer Release-Version. Hierfür benötigt es ein index.ts File welches ein weiteres File release.ts aufruft. Dies passiert sobald bun build index.ts ausgeführt wird.

1
2
3
4
5
import { getRelease } from "./release.ts" with { type: "macro" };

// The value of `release` is evaluated at bundle-time,
// and inlined into the bundle, not run-time.
const release = await getRelease();
1
2
3
4
5
6
7
export async function getRelease(): Promise<string> {
  const response = await fetch(
    "https://api.github.com/repos/oven-sh/bun/releases/latest"
  );
  const { tag_name } = await response.json();
  return tag_name;
}

Hands-On Beispiele

Anschliessend möchte ich in einem kurzen Hands-On Beispiel zeigen, wie man mit der Entwicklung mittels Bun startet.

Installation

Ich habe mich dazu entschieden die Installation einfach über brew durchzuführen. Es gibt jedoch auch die Möglichkeit dies über curl, npm, Docker oder Proto zu tun.

1
2
brew tap oven-sh/bun # for macOS and Linux
brew install bun

Projekt erstellen

Um direkt alle notwendigen Files zu haben und die wichtigsten Properties gesetzt zu haben, gibt es dieses Command:

1
bun init

Dadurch wird folgende Struktur bereitgestellt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.
├── README.md
├── bun.lockb
├── index.ts
├── node_modules
│   ├── bun-types
│   │   ├── README.md
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── types.d.ts
│   └── typescript
│       ├── LICENSE.txt
│       ├── README.md
│       ├── SECURITY.md
│       ├── ThirdPartyNoticeText.txt
│       ├── bin
│       ├── lib
│       └── package.json
├── package.json
└── tsconfig.json

Server erstellen

Innerhalb des index.ts-Files wird nun ein Endpunkt über den Port 3000 bereitgestellt, sobald bun run index.ts ausgeführt wird. Dadurch wird durch den Aufruf von http://localhost:3000 der einfache Text "Hello b-nova" bereits zurückgegeben.

1
2
3
4
5
6
7
8
9
//create server
const server = Bun.serve({
    port: 3000,
    fetch(req){
        return new Response("Hello b-nova")
    }
})

console.log(`Listening on PORT http://localhost:${server.port}`)

Im nächsten Schritt möchten wir den Output etwas aufbereiten. Für dieses Beispiel benötigen wir die zwei Packages figlet und @types/figlet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bun add figlet

Output:
bun add v1.0.14 (d8be3e51)

 installed figlet@1.7.0 with binaries:
  - figlet


 1 package installed [954.00ms]
1
bun add @types/figlet

Sobald diese installiert sind, können diese importiert und wie folgt verwendet werden:

1
2
3
4
5
6
7
8
9
import figlet from "figlet";

const server = Bun.serve({
    port: 3000,
    fetch(req){
        const body = figlet.textSync("Hello b-nova")
        return new Response(body)
    }
})

Dies erzeugt einen überarbeiteten Output: image-20231201105317202

Als letzten Schritt möchten wir nun verschiedene Router innerhalb unseres Servers anlegen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import figlet from "figlet";

const server = Bun.serve({
    port: 3000,
    fetch(req){
        var body = ''
        const url = new URL(req.url)

        if(url.pathname === '/'){
            body = figlet.textSync("Hello all")
        }
        if (url.pathname === '/b-nova'){
            body = figlet.textSync("Hello b-nova")
        }
        
        return new Response(body)
    }
})

Fazit

Insgesamt präsentiert sich Bun als beeindruckendes Toolkit für JavaScript und TypeScript, das mit innovativen Funktionen wie dem blitzschnellen Bundling, der Jest-Kompatibilität im Testen und der Performance-Steigerung durch native Implementationen glänzt.

Die durchdachte Integration von Laufzeitumgebung und Bundler sowie die Einführung von JavaScript-Makros machen Bun zu einer modernen Lösung, die Entwicklern ermöglicht, effizienten und performanten Code zu schreiben. Mit seiner Geschwindigkeit und Flexibilität bietet Bun eine vielversprechende Plattform für Entwickler, um in der sich ständig weiterentwickelnden JavaScript-Welt erfolgreich zu agieren. Aus unserer Sicht ein Must-Try! 🚀

Ricky Elfner

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.