11 Min. Lesezeit

JavaScript Laufzeitumgebungen – Browser, Node, Deno & Bun

JavaScript-Laufzeitumgebungen im Überblick: Browser, Node.js, Deno und Bun – was sie unterscheidet und wann man welche nutzt.

JavaScript wurde 1995 als kleine Skriptsprache für den Browser erfunden. Heute läuft es auf Servern, in der Cloud, auf IoT-Geräten und sogar in Datenbanken. Was all diese Umgebungen gemeinsam haben: eine Laufzeitumgebung (Runtime Environment), die den JavaScript-Code ausführt und bestimmte APIs bereitstellt. Die Runtime bestimmt, was man mit JavaScript machen kann – DOM manipulieren, Dateien lesen oder HTTP-Server starten.

Heute gibt es vier relevante JavaScript-Runtimes:

  1. Browser – Die Heimat von JavaScript
  2. Node.js – Der Klassiker für Backend
  3. Deno – Die sichere Alternative
  4. Bun – Der schnelle Newcomer

Wie eine JavaScript-Runtime funktioniert

Bevor wir die einzelnen Runtimes vergleichen, lohnt ein Blick unter die Haube. Jede Runtime besteht aus mehreren Komponenten:

┌─────────────────────────────────────────────────────────────┐
│                    JavaScript Runtime                       │
├──────────────┬──────────────────┬───────────────────────────┤
│  JS Engine   │  Event Loop      │  Platform APIs            │
│              │                  │                           │
│  • V8        │  • Call Stack    │  • Browser: DOM, fetch    │
│  • SpiderM.  │  • Task Queue   │  • Node: fs, http, net    │
│  • JSCore    │  • Microtask Q.  │  • Deno: Deno.*, fetch    │
│              │                  │  • Bun: Bun.*, fetch      │
├──────────────┴──────────────────┴───────────────────────────┤
│              libuv / tokio / Event-System                   │
├─────────────────────────────────────────────────────────────┤
│              Betriebssystem (I/O, Netzwerk, Dateisystem)    │
└─────────────────────────────────────────────────────────────┘
  • JS Engine: Parst und führt JavaScript aus (V8 bei Chrome/Node/Deno, JavaScriptCore bei Safari/Bun)
  • Event Loop: Verwaltet asynchrone Operationen
  • Platform APIs: Runtime-spezifische Funktionen (DOM im Browser, Dateisystem auf dem Server)

Die Sprache JavaScript selbst (ECMAScript) ist überall gleich – Array.map(), Promise, class funktionieren in jeder Runtime. Der Unterschied liegt in den APIs, die die Runtime bereitstellt.

1. Browser-Laufzeitumgebung

Der Browser ist die ursprüngliche Heimat von JavaScript. Hier hat man Zugriff auf alles, was mit dem Dokument und dem Benutzer zu tun hat:

Verfügbare APIs

// Das globale Objekt im Browser
window === globalThis // true (in modernem JS)

// DOM – Document Object Model
document.querySelector('button').addEventListener('click', () => {
  const div = document.createElement('div');
  div.textContent = 'Dynamisch erstellt!';
  document.body.appendChild(div);
});

// Web Storage
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');

// Fetch API (auch in anderen Runtimes verfügbar)
const response = await fetch('/api/data');
const data = await response.json();

// Web APIs
const canvas = document.querySelector('canvas').getContext('2d');
const observer = new IntersectionObserver(entries => { /* ... */ });
const worker = new Worker('/worker.js');

Browser-exklusive APIs

// Geolocation
navigator.geolocation.getCurrentPosition(pos => {
  console.log(pos.coords.latitude, pos.coords.longitude);
});

// Notifications
Notification.requestPermission().then(permission => {
  if (permission === 'granted') {
    new Notification('Neue Nachricht!');
  }
});

// Clipboard
await navigator.clipboard.writeText('Kopierter Text');

// WebSockets
const ws = new WebSocket('wss://echo.websocket.org');
ws.onmessage = (event) => console.log(event.data);

// Service Worker (PWA)
navigator.serviceWorker.register('/sw.js');

JavaScript-Engines der Browser

┌──────────────┬─────────────────┬─────────────────────────────┐
│ Browser      │ Engine          │ Besonderheiten              │
├──────────────┼─────────────────┼─────────────────────────────┤
│ Chrome/Edge  │ V8              │ Schnellste JIT-Compilation  │
│ Firefox      │ SpiderMonkey    │ Erste JS-Engine überhaupt   │
│ Safari       │ JavaScriptCore  │ Energieeffizient auf Apple  │
└──────────────┴─────────────────┴─────────────────────────────┘

Einschränkungen: Kein direkter Zugriff auf Dateisystem, Netzwerk-Sockets oder Betriebssystem-Ressourcen. JavaScript im Browser läuft in einer Sandbox – aus Sicherheitsgründen.

2. Node.js

Node.js (2009, Ryan Dahl) ermöglichte erstmals JavaScript auf dem Server. Es basiert auf der V8-Engine von Chrome und nutzt libuv für asynchrone I/O-Operationen.

Architektur

┌───────────────────────────────────────┐
│              Node.js                   │
├────────────────┬──────────────────────┤
│     V8 Engine  │  Node.js APIs        │
│                │  • fs (Dateisystem)   │
│   JavaScript   │  • http / https      │
│   ausführen    │  • net (TCP/UDP)      │
│                │  • crypto             │
│                │  • child_process      │
│                │  • stream             │
├────────────────┴──────────────────────┤
│              libuv                     │
│   (Event Loop, Thread Pool, I/O)      │
├───────────────────────────────────────┤
│         Betriebssystem                │
└───────────────────────────────────────┘

Kernfunktionen

import { readFile, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';

// Datei lesen und schreiben
const content = await readFile('./data.json', 'utf-8');
const data = JSON.parse(content);
data.lastUpdated = new Date().toISOString();
await writeFile('./data.json', JSON.stringify(data, null, 2));

// HTTP-Server erstellen
const server = createServer((req, res) => {
  if (req.url === '/api/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ name: 'Max' }, { name: 'Anna' }]));
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000, () => {
  console.log(`Server läuft auf http://localhost:3000`);
});

// Umgebungsvariablen
console.log(process.env.NODE_ENV);
console.log(process.argv);
console.log(process.cwd());

Module-Systeme

Node.js unterstützt zwei Module-Systeme – das ist historisch gewachsen und sorgt bis heute für Verwirrung:

// CommonJS (das Original, seit 2009)
const express = require('express');
module.exports = { myFunction };

// ES Modules (modern, seit Node 12+)
import express from 'express';
export { myFunction };
┌────────────────────┬──────────────────────┬────────────────────────┐
│                    │ CommonJS (CJS)       │ ES Modules (ESM)       │
├────────────────────┼──────────────────────┼────────────────────────┤
│ Syntax             │ require() / exports  │ import / export        │
│ Laden              │ Synchron             │ Asynchron              │
│ Top-Level await    │ ❌                   │ ✅                     │
│ Dateiendung        │ .js / .cjs           │ .mjs (oder package.json│
│                    │                      │ "type": "module")      │
│ Tree-Shaking       │ ❌                   │ ✅                     │
│ Standard in        │ Legacy-Projekte      │ Neue Projekte          │
└────────────────────┴──────────────────────┴────────────────────────┘

npm – Das Ökosystem

# Paketmanager
npm install express         # Paket installieren
npm init -y                 # Neues Projekt
npx create-next-app@latest  # Paket einmalig ausführen

# Alternativen
yarn add express
pnpm add express

Zahlen: Über 2,5 Millionen Pakete auf npm – das größte Software-Register der Welt.

Node.js Versionen

# Version Manager (empfohlen)
nvm install 22        # LTS installieren
nvm use 22            # Version wechseln
nvm ls                # Installierte Versionen

# Versionszyklus
# Gerade Nummern (20, 22) = LTS (Long Term Support)
# Ungerade Nummern (21, 23) = Current (experimentell)

3. Deno

Deno (2020) wurde von Ryan Dahl – dem Erfinder von Node.js – als dessen “Korrektur” entwickelt. In seinem berühmten Talk “10 Things I Regret About Node.js” listete er die Designfehler auf, die er mit Deno beheben wollte.

Die 10 Dinge, die Dahl an Node.js bereut

1. Promises nicht beibehalten (wurden entfernt, dann wieder hinzugefügt)
2. Sicherheit – Node hat Zugriff auf alles
3. Das Build-System (GYP)
4. package.json wurde zu komplex
5. node_modules – riesig und verschachtelt
6. require() ohne Dateiendung
7. index.js als Standard
8. window-Objekt nicht genutzt
9. Keine native TypeScript-Unterstützung
10. Kein eingebauter Linter/Formatter

Kernkonzepte

// Deno nutzt TypeScript nativ – kein Transpiler nötig!
interface User {
  id: number;
  name: string;
  email: string;
}

// Datei lesen (Deno-eigene API)
const content: string = await Deno.readTextFile('./users.json');
const users: User[] = JSON.parse(content);

// HTTP-Server (Web-Standard-APIs)
Deno.serve({ port: 3000 }, (req: Request): Response => {
  const url = new URL(req.url);

  if (url.pathname === '/api/users') {
    return new Response(JSON.stringify(users), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  return new Response('Not Found', { status: 404 });
});

Permissions – Sicherheit by Design

Das wichtigste Unterscheidungsmerkmal: Deno blockiert standardmäßig alle Zugriffe.

# Nur Netzwerkzugriff erlauben
deno run --allow-net server.ts

# Nur bestimmte Domains
deno run --allow-net=api.example.com app.ts

# Dateisystem nur lesend
deno run --allow-read=/data app.ts

# Umgebungsvariablen nur bestimmte
deno run --allow-env=DATABASE_URL,API_KEY app.ts

# Alles erlauben (nur für Entwicklung!)
deno run -A app.ts
┌─────────────────────┬──────────────────────────────────────┐
│ Permission          │ Erlaubt                              │
├─────────────────────┼──────────────────────────────────────┤
│ --allow-read        │ Dateisystem lesen                    │
│ --allow-write       │ Dateisystem schreiben                │
│ --allow-net         │ Netzwerkzugriff                      │
│ --allow-env         │ Umgebungsvariablen                   │
│ --allow-run         │ Subprozesse starten                  │
│ --allow-ffi         │ Foreign Function Interface           │
│ --allow-sys         │ Systeminformationen                  │
└─────────────────────┴──────────────────────────────────────┘

Eingebaute Tools

# Formatter (wie Prettier, aber eingebaut)
deno fmt

# Linter (wie ESLint, aber eingebaut)
deno lint

# Test-Runner (wie Jest, aber eingebaut)
deno test

# Bundler
deno bundle app.ts app.bundle.js

# Dokumentation generieren
deno doc server.ts

# Dependency-Inspektor
deno info https://deno.land/std/http/server.ts

Deno 2.0 – Node.js-Kompatibilität

Deno 2.0 (2024) brachte einen großen Paradigmenwechsel – volle Node.js-Kompatibilität:

// npm-Pakete direkt importieren (ohne node_modules!)
import express from "npm:express@4";
import chalk from "npm:chalk@5";

// Node.js built-in Module
import { readFileSync } from "node:fs";

// package.json wird jetzt auch unterstützt
// deno.json als Konfiguration

4. Bun

Bun (2022, Jarred Sumner) ist die neueste Runtime und fokussiert sich auf Geschwindigkeit. Statt V8 nutzt Bun die JavaScriptCore Engine (aus Safari) und ist in Zig geschrieben – einer Low-Level-Sprache.

Warum ist Bun so schnell?

┌─────────────────────────────────────────────────────────┐
│ Node.js                                                  │
│ JavaScript → V8 → C++ → libuv → OS                     │
│                                                          │
│ Bun                                                      │
│ JavaScript → JSC → Zig → OS (weniger Abstraktionsebenen)│
└─────────────────────────────────────────────────────────┘
  • JavaScriptCore startet schneller als V8
  • Zig statt C++ reduziert Overhead
  • Eigener Paketmanager statt npm-CLI
  • Native APIs für häufige Operationen (SQLite, Hashing, etc.)

Kernfunktionen

// Bun-spezifische APIs
const file = Bun.file('./data.json');
const content = await file.text();
const data = JSON.parse(content);

// Dateien schreiben
await Bun.write('./output.txt', 'Hello from Bun!');

// Blitzschneller HTTP-Server
Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);

    if (url.pathname === '/api/data') {
      return Response.json({ message: 'Hello from Bun!' });
    }

    return new Response('Not Found', { status: 404 });
  },
  error(error) {
    return new Response(`Error: ${error.message}`, { status: 500 });
  }
});

// SQLite eingebaut – kein npm-Paket nötig
import { Database } from 'bun:sqlite';
const db = new Database('app.sqlite');
db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
db.run('INSERT INTO users (name) VALUES (?)', ['Max']);
const users = db.query('SELECT * FROM users').all();

// Passwort-Hashing eingebaut
const hash = await Bun.password.hash('meinPasswort');
const valid = await Bun.password.verify('meinPasswort', hash);

Bun als Paketmanager

# Installation (bis zu 25x schneller als npm)
bun install              # Alle Pakete
bun add express          # Einzelnes Paket
bun add -d vitest        # Dev-Dependency
bun remove express       # Paket entfernen

# Benchmark: node_modules für ein Next.js-Projekt
# npm install:  45s
# yarn install: 22s
# pnpm install: 15s
# bun install:   2s

Bun als Bundler und Transpiler

# TypeScript direkt ausführen (kein tsc/ts-node nötig)
bun app.ts

# Bundeln für den Browser
bun build ./src/index.ts --outdir ./dist --minify

# JSX/TSX direkt ausführen
bun app.tsx

Node.js-Kompatibilität

// Die meisten Node.js-APIs funktionieren
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createServer } from 'node:http';

// Die meisten npm-Pakete funktionieren
import express from 'express';
import lodash from 'lodash';

Einschränkung: Einige native Node.js-Addons (C++ Bindings) funktionieren noch nicht. Die Kompatibilität verbessert sich aber mit jedem Release.

Vergleichstabelle

┌──────────────────┬──────────┬────────────────┬────────────────┬───────────┐
│ Feature          │ Browser  │ Node.js        │ Deno           │ Bun       │
├──────────────────┼──────────┼────────────────┼────────────────┼───────────┤
│ JS Engine        │ V8/JSC/SM│ V8             │ V8             │ JSC       │
│ TypeScript       │ ❌       │ Via Transpiler │ ✅ Native      │ ✅ Native │
│ DOM-Zugriff      │ ✅       │ ❌             │ ❌             │ ❌        │
│ Dateisystem      │ ❌       │ ✅             │ ✅ (Permission)│ ✅        │
│ npm-Pakete       │ ❌       │ ✅             │ ✅ (seit 2.0)  │ ✅        │
│ Web APIs         │ ✅       │ ⚠️ Teilweise   │ ✅             │ ✅        │
│ Sicherheit       │ Sandbox  │ ❌             │ ✅ Permissions │ ❌        │
│ Eingebaute Tools │ DevTools │ ❌             │ ✅ Viele       │ ✅ Viele  │
│ SQLite           │ ❌       │ ❌ (npm nötig) │ ❌ (npm nötig) │ ✅ Native │
│ Geschwindigkeit  │ –        │ Basis          │ ~1.5x          │ ~3-4x     │
│ Reife            │ 30 Jahre │ 15+ Jahre      │ 4+ Jahre       │ 2+ Jahre  │
│ Ökosystem        │ –        │ Riesig (npm)   │ Wachsend       │ npm-komp. │
└──────────────────┴──────────┴────────────────┴────────────────┴───────────┘

Gemeinsame APIs – Der Web-Standard

Ein wichtiger Trend: Alle Runtimes konvergieren bei den Web Platform APIs. Das bedeutet, Code der diese APIs nutzt, läuft überall:

// Diese APIs funktionieren in ALLEN modernen Runtimes:

// fetch() – HTTP-Requests
const res = await fetch('https://api.example.com/data');

// URL – URL-Parsing
const url = new URL('https://example.com/path?key=value');

// TextEncoder / TextDecoder
const encoder = new TextEncoder();
const bytes = encoder.encode('Hello');

// crypto – Kryptografie
const uuid = crypto.randomUUID();
const hash = await crypto.subtle.digest('SHA-256', bytes);

// AbortController – Requests abbrechen
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await fetch(url, { signal: controller.signal });

// structuredClone – Deep Copy
const copy = structuredClone({ nested: { data: [1, 2, 3] } });

Welche Runtime wählen?

Entscheidungsbaum

Brauchst du DOM-Zugriff?
├── Ja → Browser
└── Nein
    ├── Bestehendes Projekt mit npm-Dependencies?
    │   ├── Ja → Node.js (oder Bun als Drop-in)
    │   └── Nein
    │       ├── Sicherheit besonders wichtig?
    │       │   ├── Ja → Deno
    │       │   └── Nein
    │       │       ├── Performance kritisch?
    │       │       │   ├── Ja → Bun
    │       │       │   └── Nein → Node.js (größtes Ökosystem)
    │       │       └──
    │       └──
    └──

Zusammenfassung

  • Browser: Frontend-Anwendungen, DOM-Manipulation, Web APIs
  • Node.js: Produktionsreife Backend-Apps, größtes Ökosystem, beste Stabilität
  • Deno: Neue TypeScript-Projekte, Sicherheit wichtig, moderne Toolchain
  • Bun: Performance-kritische Anwendungen, schnelle Entwicklung, All-in-One Tool

Praktisches Beispiel: Gleicher Server in allen Runtimes

// === Node.js ===
import { createServer } from 'node:http';
createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js!');
}).listen(3000);

// === Deno ===
Deno.serve({ port: 3000 }, () => {
  return new Response('Hello from Deno!');
});

// === Bun ===
Bun.serve({
  port: 3000,
  fetch() {
    return new Response('Hello from Bun!');
  }
});

Deno und Bun nutzen die Web-Standard Response-API, während Node.js seine eigene http-API hat. Das zeigt den Trend: Neue Runtimes setzen auf Web-Standards, Node.js behält seine eigenen APIs aus Kompatibilitätsgründen.

Fazit

Die JavaScript-Welt hat sich von “nur im Browser” zu einem vielseitigen Ökosystem entwickelt. Node.js bleibt der Standard für produktionsreife Anwendungen mit dem mit Abstand größten Ökosystem. Deno bringt Sicherheit und TypeScript-First, während Bun mit beeindruckender Performance punktet. Alle drei Server-Runtimes nähern sich über Web-Standards immer weiter aneinander an – Code, der heute für eine Runtime geschrieben wird, lässt sich zunehmend leichter portieren.

Am besten probiert man alle aus und wählt je nach Projektanforderung. Wer ein neues Projekt startet und noch keine Legacy-Dependencies hat, sollte Deno oder Bun eine Chance geben.