Eine Weather CLI, fünf Toolchains: Rust, Go, Zig, Bun und Node.js
Wie derselbe kleine HTTP-plus-JSON CLI in Rust, Go, Zig, Bun und Node.js aussieht sowie was Größe der Binärdatei, Kompilierzeit und Einrichtungshürden für Ihre Wahl bedeuten.
Mikro-Benchmarks wie ein Fibonacci-Loop sagen sehr wenig darüber aus, was es kostet, ein echtes Kommandozeilenwerkzeug zu veröffentlichen. Ein besseres Testverfahren ist eine kleine Utility, die mit dem Netzwerk kommuniziert, JSON decodiert, etwas Mathematik durchführt und saubere Ausgaben ausgibt – und die in mehreren Sprachen identisch implementiert wird. Diese Anleitung folgt genau diesem Experiment mit Rust, Go, Zig, Bun und Node.js, damit Sie beurteilen können, welche Toolchain aufgrund der Binärgröße, des Kompilierzeitraums, der Laufzeitauslastung sowie – was am wichtigsten ist – des Grades an Hindernissen zwischen einem leeren Verzeichnis und einer Binärdatei, die Ihre Nutzer ausführen können, am besten zu Ihrem nächsten CLI passt.
Die wichtigsten Ergebnisse sollten gleich zu Beginn genannt werden. Die endgültigen Binärdateien lagen zwischen 1,2 MB und 45 MB, und das vollständige Auslassen der Kompilierung bedeutet, dass die Benutzer einen Laufzeitumfang von etwa 100 MB installieren müssen. Die Zeit für eine saubere Kompilierung lag zwischen 0,8 Sekunden und 28 Sekunden. Die praktischste Empfehlung am Ende ist somit nicht die Sprache mit der schnellsten Ausführung.
Das Testwerkzeug und warum es eine faire Last darstellt
Das Tool heißt wx. Man gibt ihm einen Stadtnamen ein; es wandelt diesen Namen über die Open-Meteo-Geokodierungs-API in Koordinaten um, ruft die aktuellen Wetterbedingungen über die Open-Meteo-Prognose-API ab und gibt das Ergebnis zusammen mit einer berechneten „empfundenen“ Temperatur aus. Eine typische Aufrufweise sieht wie folgt aus:
$ wx reykjavik
Reykjavik, Iceland
Temperature: 4.2C (feels like -0.4C)
Wind: 24 km/h NNW
Humidity: 68%
Das Tool ist absichtlich klein, berührt aber die vier Bereiche, in denen sich die Ergonomie der CLI tatsächlich zwischen den Ökosystemen unterscheidet:
- Ein HTTP-Client mit TLS. Es sind zwei HTTPS-Anfragen erforderlich, eine für die Geokodierung und eine für die Vorhersage.
- JSON-Decodierung. Die Antworten werden auf typisierte Strukturen abgebildet, anstatt als lose Objekte behandelt zu werden.
- Echte Berechnung. Die scheinbare Temperatur wird mithilfe einer Formel mit Entscheidungsbäumen berechnet, nicht durch Zeichenkettenverknüpfung.
- Terminal-Ausgabe. ANSI-Farben und ausgerichtete Spalten – das, was die Benutzer tatsächlich sehen.
Eine Sprache, die all diese vier Aspekte nicht komfortabel handhaben kann, ist keine gute Wahl für eine CLI – egal wie schnell sie numerische Schleifen ausführt.
Der gemeinsame Logikteil
Jede Version verwendet dieselbe dreigliedrige Regel. Unter 10 °C wird die Windkälteformel von Environment Canada angewandt. Über 27 °C wird die Wärmeindex-Regression von NOAA Rothfusz verwendet, ausgedrückt in Grad Celsius mit der Feuchtigkeit als Prozentsatz. Dazwischen wird die Rohtemperatur unverändert zurückgegeben. Die TypeScript-Version, die sowohl bei den Bun- als auch Node.js-Builds verwendet wird, ist hier dargestellt; die anderen Sprachen wenden dieselbe Arithmetik an.
function feelsLike(tempC: number, windKmh: number, humidity: number): number {
if (tempC < 10) {
// Wind chill (Environment Canada formula)
const v = windKmh ** 0.16;
return 13.12 + 0.6215 * tempC - 11.37 * v + 0.3965 * tempC * v;
}
if (tempC > 27) {
// Heat index (NOAA Rothfusz regression, in Celsius)
const t = tempC;
const r = humidity;
return -8.784 + 1.611 * t + 2.338 * r - 0.146 * t * r
- 0.0123 * t * t - 0.0164 * r * r + 0.00221 * t * t * r
+ 0.000725 * t * r * r - 0.00000358 * t * t * r * r;
}
return tempC; // between 10C and 27C, raw temperature
}
Zwei Aspekte verdienen besondere Aufmerksamkeit. Erstens sind die Regimegrenzen der Punkt, an dem eine nachlässige Umsetzung Fehler macht, wodurch sie eine gute Prüfung auf Korrektheit bei der Vergleich von Implementierungen darstellen. Zweitens haben beide Formeln Gültigkeitsbereiche, die diese einfache Version ignoriert: Die Windkälteformel ist für Windgeschwindigkeiten von etwa 5 km/h und mehr vorgesehen, während die Rothfusz-Regression nur für heiße, recht feuchte Bedingungen geeignet ist. Für ein einfaches Wettertool ist das akzeptabel, doch eine Produktionsversion sollte in diesen Bereichen entweder begrenzen oder auf die Rohtemperatur zurückgreifen.
Wie sich jede Implementierung beim Aufbau angefühlt hat
Der gesamte Code in allen fünf Sprachen umfasst etwa 360 Zeilen, weshalb nur die aufschlussreichen Ausschnitte gezeigt werden. Entscheidend ist, wo jedes Umfeld geholfen hat und wo es Hindernisse darstellte.
Rust mit reqwest, serde und einem auf derive basierenden Argumentparser
Die Rust-Build-Struktur verwendet ein beliebtes, auf ‚derive‘ basierendes Paket zur Argumentverarbeitung, reqwest für HTTP-Anfragen und serde zur Deserialisierung. Der folgende Auszug zeigt das Muster, das Rust für diese Art von Arbeit besonders geeignet macht: Die ‚derive‘-Makros erzeugen aus einfachen Strukturdefinitionen sowohl den CLI-Parser als auch die JSON-Decoder, und die Feldtypen werden zur Kompilierzeit überprüft.
#[derive(Parser)]
#[command(name = "wx", about = "Weather lookup")]
struct Cli {
city: String,
}
#[derive(Deserialize)]
struct WeatherResponse {
current: CurrentWeather,
}
#[derive(Deserialize)]
struct CurrentWeather {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
}
Das gesamte Programm umfasst etwa 95 Zeilen, die sowohl API-Aufrufe als auch die Logik zur Temperaturberechnung behandeln. Der asynchrone Laufzeitmechanismus tokio muss explizit aktiviert werden, während Node.js seinen Event-Loop vor dem Benutzer verbirgt. Diese Explizitheit ist zwar nützlich zur Steuerung, trägt aber zum Binärgrößezuwachs bei.
Überraschend war die Standardausgabe: Ein einfacher Befehl cargo build --release erzeugte ein 8,4 MB großes Binärdatei. Durch Aktivierung der Linkzeitoptimierung sowie des Entfernens von Symbolen in Cargo.toml sank die Größe auf 3,8 MB. Diese Einstellungen sind bei den Entwicklern von Rust-Tools gut bekannt, doch ein Anfänger würde vermutlich die größere Datei verbreiten, ohne zu wissen, dass eine kleinere Version nur zwei Zeilen entfernt ist.
Nur die Standardbibliothek verwenden
Der Go-Compiler benötigt überhaupt keine Drittanbieter-Pakete. net/http, encoding/json und os.Args reichen aus, um die gesamte Aufgabe zu erledigen. Der angezeigte Codeausschnitt zeigt die Response-Struktur, bei der Tags JSON-Schlüssel auf typische Go-Feldnamen abbilden, sowie den Anfang von main mit einer minimalen Nutzungskontrolle.
type WeatherResponse struct {
Current struct {
Temperature float64 `json:"temperature_2m"`
WindSpeed float64 `json:"wind_speed_10m"`
Humidity int `json:"relative_humidity_2m"`
WindDir float64 `json:"wind_direction_10m"`
} `json:"current"`
}
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: wx <city>")
os.Exit(1)
}
city := os.Args[1]
// geocode, fetch weather, compute, print
}
Das fertige Programm umfasst 68 Zeilen. Ein Muster dominiert es: Die Prüfung if err != nil { log.Fatal(err) } kommt fünfmal vor – jeweils einmal für die Geocodierungsanfrage, ihren Inhalt, die Dekodierung, die Wettervorhersageanfrage sowie den Inhalt der Vorhersage. Diese Wiederholung stellt kein echtes Problem dar, doch es handelt sich dabei um die häufigste Zeile in praktisch jedem Go CLI.
Ein funktionsfähiges Binärdatei entstand bereits nach etwa zehn Minuten. Die Geschwindigkeit rührte nicht davon her, dass Go eine minimale Sprache ist (sie hat starke Ansichten, die nicht jedem gefallen), sondern davon, dass es nichts zu entscheiden gab: keine Bibliotheksvergleiche, kein Herunterladen von Abhängigkeiten und keine Konfiguration.
Zig 0.16 mit std.http.Client und std.json
Zig, getestet in Version 0.16, war die aufschlussreichste Version, aber auch die langsamste zum Abschluss. Der Auszug zeigt die Response-Struktur sowie den Anfang von main, wo ein Debug-Alloker erstellt und anschließend weitergegeben wird. Das ist das kennzeichnende Merkmal von Zig: Jede Funktion, die Heap-Memory allozieren kann, erhält einen Alloker-Parameter, sodass das Eigentumsverhältnis am Speicher stets in der Aufrufsignatur sichtbar ist.
const WeatherResponse = struct {
current: struct {
temperature_2m: f64,
wind_speed_10m: f64,
relative_humidity_2m: u8,
wind_direction_10m: f64,
},
};
pub fn main() !void {
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
const allocator = debug_allocator.allocator();
// Every function that might allocate takes `allocator` as a parameter.
// This is Zig's deal: you control memory, always.
}
Das Programm wuchs auf 108 Zeilen an und war damit das längste der fünf. Die Kompilierung selbst dauerte nur 0,8 Sekunden – das schnellste Ergebnis im Vergleich – doch der gesamte Build-Prozess beanspruchte mehr als eine Stunde Arbeitszeit. Der Grund dafür war TLS. std.http.Client enthält zwar seine eigene TLS-Implementierung, benötigt aber dennoch vertrauenswürdige Root-Zertifikate; auf dem Testrechner konnte es den System-CA-Bundle nicht finden. Das einzige Symptom war error.TlsInitializationFailed ohne weitere Hinweise. Die Lösung, die Zertifikate explizit über std.crypto.Certificate.Bundle zu laden und diesen Bundle an den Client weiterzugeben, tauchte erst in einer Diskussion zu einem GitHub-Issue auf. Ihre Ergebnisse können je nach Plattform unterschiedlich ausfallen, doch die Lektion bleibt: Ein schneller Compiler kann keine Zeit wieder aufholen, die durch undurchsichtige Laufzeitfehler verloren geht.
Die explizite Übermittlung des Allocators eignet sich hervorragend für Software, in der das Verhalten des Speichers von Bedeutung ist. Für ein Werkzeug, das nur einige Zeichenketten und einen JSON-Puffer zuweist, fügt dies meist nur unnötige Komplexität hinzu. Zig verfügt zwar über einen integrierten Paketmanager basierend auf build.zig.zon, der seit Version 0.11 verfügbar ist, doch das Ökosystem bleibt noch spärlich; es gab keine gepflegte Terminal-Farbenbibliothek, weshalb stattdessen ein etwa 40 Zeilen langer ANSI-Hilfsprogramm aus einem Gist kopiert wurde.
Bun mit einer einzigen TypeScript-Datei
Die Bun-Version ist die kürzeste und am leichtesten lesbare. Sie liest die Stadt aus Bun.argv ein, gibt bei Fehlen eine Verwendungsanweisung aus und verwendet anschließend zweimal die eingebaute Funktion fetch, um jeweils die Antwort mit .json() zu decodieren. Durch das await auf höchster Ebene gibt es keine Wrapper-Funktion.
const city = Bun.argv[2];
if (!city) {
console.error("usage: wx <city>");
process.exit(1);
}
// Geocode city name to coordinates
const geoRes = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`
);
const geo = await geoRes.json();
const { latitude, longitude } = geo.results[0];
// Fetch weather
const wxRes = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,wind_speed_10m,relative_humidity_2m,wind_direction_10m`
);
const data = await wxRes.json();
Die gesamte Datei umfasst 47 Zeilen, einschließlich beider Anfragen, und sie lief etwa acht Minuten lang, wobei die meiste Zeit für die Berechnung der Temperaturformel benötigt wurde. Beachten Sie jedoch, was der Auszug nicht tut: Er überprüft niemals res.ok, und geo.results[0] ist undefiniert, wenn der Geocoder keine Übereinstimmung findet. Dadurch führt ein falsch geschriebener Stadtname zu einem Fehler bei der Dekonstruktion anstelle einer freundlichen Meldung. Die Kürze ist zwar gegeben, doch eine Produktions-CLI benötigt diese wenigen zusätzlichen Zeilen.
Das Problem mit Bun liegt in der Verbreitung. bun build --compile erzeugt für dieses kleine Tool eine eigenständige Ausführbare von etwa 45 MB, da die Ausgabe den JavaScriptCore-Engine und den Bun-Runtime zusammenpackt. Das sind etwa sieben Mal so viel wie das Go-Binärdatei und fast vierzig Mal so viel wie die Zig-Version. Wenn Ihre Nutzer bereits Bun installiert haben, vermeidet das Direktausführen der .ts-Datei dieses Problem völlig.
Node.js, das keine eigene Aufzählung benötigt
Die Node.js-Implementierung ist im Grunde genommen derselbe Bun-Code, da Node.js ab Version 18 eine native fetch-Funktion mitgeliefert hat. Die wesentlichen Unterschiede liegen im Laufzeitumfeld:
- Startkosten. Der größte Teil der Differenz im Gesamtlaufzeitbedarf entsteht durch den Start des Prozesses selbst: 82 ms bei Node.js gegenüber 24 ms bei Bun, wobei die simulierten Netzwerkaufrufe nur wenige Millisekunden hinzufügen. Bei einem einzelnen interaktiven Befehl fällt das nicht auf; innerhalb einer Shell-Schleife, die das Tool Hunderte Male ausführt, summieren sich diese Zeiten jedoch auf.
.ts-Datei ohne tsx oder ts-node ausgeführt werden kann; zum Zeitpunkt der Erstellung wurde diese Funktion in der 24er LTS-Linie als stabil beschrieben. Nur lösbare Syntax wird unterstützt: Anmerkungen verschwinden, doch Konstrukte, die JavaScript erzeugen – wie z. B. Enums, Namespaces und Parametereigenschaften – benötigen weiterhin einen Transpiler. Bun geht ohne Konfiguration noch weiter und beherrscht JSX, Dekoratoren sowie Pfadaliasse. Für eine detailliertere Übersicht darüber, was die native Entfernung umfasst, siehe was Node.js native TypeScript-Unterstützung tatsächlich kann und nicht kann.Wenn man Node.js ausschließlich aus der Perspektive einer völlig neuen CLI betrachtet, bietet es denselben Code wie Bun – allerdings mit langsamerer Startzeit und komplexerer Verbreitung. Das ist jedoch ein sehr eng gefasstes Urteil. Wenn Ihr Team bereits auf Node.js setzt, von dessen npm-Kompatibilitätsgarantien abhängt oder eine bestehende CLI darauf betreibt, können diese Faktoren leicht die 60 ms lange Startzeitdifferenz überwiegen.
Messumgebung und die gemeldeten Zahlen
Die Zeiten wurden auf einem M3 MacBook Pro mit 18 GB RAM unter macOS 26 erfasst, wobei hyperfine mit dem Befehl hyperfine --warmup 3 --min-runs 50 './wx reykjavik' verwendet wurde. Um Netzwerkstörungen zu vermeiden, bezogen sich beide API-Aufrufe auf einen lokalen Mock-HTTP-Server, der festgelegte JSON-Daten zurückgab. Die angegebene Gesamtlaufzeit bezieht sich auf die Zeit vom Start bis zum Beenden des Prozesses einschließlich des Starts. Die Entwicklungszeiten sind grobe Schätzungen und keine gemessenen Werte, daher sollten die Verhältnisse verglichen werden, nicht die Minuten.
Die wichtigsten Ergebnisse des Tests:
- Rust: etwa 95 Zeilen, 3,8 MB nach Optimierung (8,4 MB standardmäßig), 28 Sekunden für einen sauberen Build, Ausführungsdauer von 4,8 ms.
- Go: 68 Zeilen, 6,2 MB, Kompilierzeit unter einer Sekunde, Ausführungsdauer von 5,2 ms, Fertigstellung in etwa 10 Minuten.
Die Größenangaben hängen von den Kompilierungsflaggen ab. Rust benötigte lto = true und strip = true unter [profile.release]. Go wurde mit go build -ldflags='-s -w' kompiliert, um Debug-Informationen wegzulassen. Zigs 1,2 MB entsprechen einer ReleaseSmall-Kompilierung; es bleibt auch bei TLS sehr klein, da der HTTP-Client und der Krypto-Code in der Standardbibliothek enthalten sind und statisch verknüpft werden, wobei unnötiger Code entfernt wird – anstelle von Bibliotheken wie OpenSSL. Eine ReleaseSafe-Kompilierung, die Laufzeit-Sicherheitsprüfungen beibehält, hat eine Größe von etwa 2,1 MB.
Was die Benchmark-Zahlen verbergen
Zig gewinnt auf dem Papier, verliert in der Praxis
Laut den Messwerten erzeugte Zig die kleinste und eine der schnellsten Binärdateien. Die Stunde, die für 108 Zeilen benötigt wurde, erzählt eine andere Geschichte:
- Das Problem mit dem Zertifikatspaket kostete mehr als 40 Minuten – aufgrund eines einzeiligen Fehlers.
ReleaseSmall-Version, die 1,2 MB groß ist, enthält keine Stack-Traces; ReleaseSafe behält zwar die Panic-Traces bei, ist aber 2,1 MB groß.defer-Anweisungen führten zu Lecks, die erst dann sichtbar wurden, als der Debug-Allokator sie beim Beenden meldete.Für ein langfristig genutztes Tool, bei dem man jede Allokation sowie jeden Byte der Ausgabe kontrollieren möchte, lohnt sich diese Explizitheit im Laufe der Zeit. Für etwas, das bis zum Mittag funktionieren soll, eignet es sich derzeit nicht gut. Die Spracharchitektur ist elegant – das umgebende Ökosystem ist einfach noch jung.
Go ist nie in einer Sache am besten, gewinnt aber insgesamt dennoch
Go wird in unter einer Sekunde kompiliert, erzeugt ein vernünftiges, eigenständiges Binärdatei, benötigt nur die Standardbibliothek und funktionierte bereits nach zehn Minuten. Es ist größer als optimiertes Rust (6,2 MB gegenüber 3,8 MB) und etwas langsamer (5,2 ms gegenüber 4,8 ms), doch ein Mensch kann diesen Unterschied bei einem Tool, das in einstelligen Millisekunden beendet wird, nicht wahrnehmen.
Für die Cross-Kompilierung reicht eine einzige Umgebungsvariable, zum Beispiel GOOS=linux go build. Rust kommt mit cargo build --target x86_64-unknown-linux-gnu oder dem Hilfsprogramm cargo-zigbuild nahe heran, und Zig hat vermutlich die stärksten Vorteile bei der Cross-Kompilierung, da es seinen eigenen Linker und libc mitbringt. Der Unterschied ist, dass Go weder zusätzliche Tools noch eine Einrichtung erfordert. Das ist das wiederkehrende Muster: Go erreicht selten die Spitze in irgendeiner einzelnen Kennzahl, hat aber insgesamt den geringsten Aufwand.
Bun ist lokal hervorragend, aber schwierig zu verteilen
Bun lieferte den saubersten Code in kürzester Zeit. Für ein persönliches Skript, das als .ts-Datei in ~/bin liegt, ist es kaum zu übertreffen. Für die Verbreitung stellt 45 MB für eine Wetterabfrage jedoch ein echtes Manko dar, und da fast der gesamte Umfang aus dem eingebetteten Motor besteht, gibt es keine praktische Möglichkeit, das Programm zu verkleinern – außer durch eine leichtere Ausführungsversion vom Bun-Team, die zum Zeitpunkt des Tests noch nicht verfügbar war.
Rusts Argumente für kleine Tools haben an Gewicht verloren
Vor einigen Jahren beruhten die Vorteile von Rust-CLIs auf Geschwindigkeit, Sicherheit und kleinen Binärdateien. Bei I/O-intensiven Tools sind diese Vorteile heute weniger eindeutig:
- Go erreicht eine praktisch vergleichbare Geschwindigkeit wie Rust.
- Zig erzeugt ohne jegliche Anpassungen kleinere Binärdateien.
- Eine saubere Kompilierzeit von 28 Sekunden für ein 95 Zeilen langes Programm ist eine zu hohe Belastung für schnelle Hilfsprogramme.
Rust bleibt weiterhin hervorragend, wenn ein Tool Tausende von Nutzern anzieht und über Jahre hinweg gewartet wird, wobei das Typsystem ständig unerwartete Fälle erfasst. ripgrep, fd, bat, delta und hyperfine sind allesamt Rust-basierte CLI-Tools, die über lange Zeiträume ernsthaft gewartet werden und nicht an einem Wochenende entwickelt wurden. Für schnelle Projekte lohnt sich der Aufwand selten; für ein weit verbreitetes Tool ist es jedoch möglicherweise die Option, bei der am wenigsten subtile Fehler entstehen.
Die Wahl des Toolchains für Ihre nächste CLI
Die Entscheidung hängt weniger von der reinen Geschwindigkeit ab als davon, wer das Tool nutzen wird und wie es zu ihnen gelangt:
- Wählen Sie Go als Standard, wenn das Ziel die geringste Gesamtkostenstruktur vom leeren Verzeichnis bis zum verteilten Binärdatei ist: Funktionsfähiger Code in Minuten, Kompilierzeiten unter einer Sekunde, eine einzige 6,2 MB große Datei ohne Abhängigkeiten sowie einfache Cross-Compilation.
Jede Implementierung ist klein genug, um an einem Nachmittag aus den oben genannten Fragmenten zusammen mit einem Mock-Server und einer Hyperfine-Skript neu erstellt zu werden. Ihre genauen Werte variieren je nach Hardware, Betriebssystem und Versionen der Toolchain, doch das relative Bild sollte stabil bleiben: Zig ist am kleinsten, Bun am größten, Go weist die geringsten Einschränkungen auf.
Haupterkenntnisse
- Messen Sie den gesamten Pfad zu einem bereitgestellten Binärdatei, nicht nur die Ausführungszeit; Einrichtung, Debugging und Verbreitung spielen bei kleinen Tools eine dominante Rolle.
- Standard-Einstellungen beim Kompilieren können die Größe der Binärdatei verdoppeln, daher sollten Sie sich mit den Veröffentlichungsflaggen der von Ihnen gewählten Toolchain vertraut machen.
- Binärdateien, die bei Bun oder Node.js SEA lauffertungsabhängig erstellt werden, enthalten den gesamten Engine-Code – was weitaus wichtiger ist als ihre Startzeit.
- Überprüfen Sie Eingaben sowie HTTP-Antworten auch in kurzen Skripten; die kürzeste Implementierung weist oft keine Fehlerbehandlung auf.
- Betrachten Sie hier spezifische Versionszustände und -größen als Momentaufnahme und prüfen Sie sie vor einer Entscheidung erneut gegenüber den aktuellen Versionen.
Verwandte Literatur
- Edge Isolates und Wasm gegen Node.js: Laufzeit-Abwägungen und Produktionspraktiken — Erklärt, wie V8-Isolaten und WebAssembly an der Edge-Edge im Vergleich zu containerbasiertem Node.js leistungsfähiger sind, und behandelt anschließend die Betriebspraktiken, die Node.js für die Produktion bereit machen.
- Vergleich von Node.js, Deno und Bun: Benchmarks, Abwägungen und Migrationsstrategie — Erläutert die tatsächlichen architektonischen Unterschiede zwischen Node.js, Deno und Bun, was die Benchmarks von 2025 aufzeigen, sowie wie man entscheidet, ob und wann migriert werden soll.