Ist Deno wirklich so viel besser, sicherer und schneller als Node.js?

12.10.2022Valentin Neher
Tech Deno JavaScript Node.js Web Development

cover_wide

Figure: Quelle: https://deno.land/artwork

Deno (/ˈdiːnoʊ/, “dino”) ist eine Runtime für JavaScript und TypeScript (sowie WebAssembly), die Mitte 2018 von Ryan Dahl, dem Erfinder von Node.js, angekündigt, und genau zwei Jahre später in der Version 1.0 veröffentlicht wurde. Bei Deno steht genau wie bei Node.js der Gedanke im Vordergrund, JavaScript auch im Backend ausführen zu können.

Jetzt liegt natürlich die Frage nahe, wieso Dahl Deno kreiert hat, obwohl Node.js ja grundsätzlich den genau gleichen Usecase hat. Dies hat diverse Gründe, die er in seinem Talk an der JsConf EU “10 Things I Regret About Node.js” (2018) humorvoll zusammenfasst. Dahl hat ein Flair für dynamische Programmiersprachen, wobei in seiner Sicht JavaScript die beste dynamische Programmiersprache ist. Als er 2012 aufhörte, an der Weiterentwicklung von Node.js involviert zu sein, und sich stattdessen mit der entwicklung performanter Server in Golang beschäftigte, jedoch in den sechs Monaten vor dem oben genannten JsConf EU Talk wieder stärker mit Node.js auseinandersetzte, bemerkte er die Designfehler, die ihm mit Node.js unterlaufen waren. Seit den Anfängen von Node.js im Jahr 2008 hat sich die Webentwicklung weiterentwickelt; Bereiche wie “Security”, die beim Design von Node.js nicht gerade im Vordergrund standen, haben an Stellenwert zugenommen. Zusammengefasst haben die Erkenntnisse aus der Entwicklung von Node.js in Kombination mit Dahl’s anhaltender Begeisterung mit JavaScript, ihn dazu bewogen, Deno zu kreieren, und seine Fehler von damals zu korrigieren.

Um Deno zu verstehen, müssen wir uns zuerst einmal anschauen, welche Designfehler von Node.js Dahl zur Entwicklung von Deno bewogen haben.

Ryan Dahl’s Node.js Designfehler

  • Promises: Frühere Einführung von Promises hätte die Einführung von async/await beschleunigen können.
  • Security: Ein von Beginn an stärkerer Fokus auf Security hätte auf dem bereits vorhandenen Potential der Node zugrundeliegenden V8 Runtime aufbauen können.
  • Node Package Manager (npm): Node Module sind in einer zentralisierten privat kontrollierten Datenbank gespeichert. Dass npm Module selbst wiederum sehr viele Dependencies haben können macht sie unübersichtlich und anfällig für unsicheren und potentiell bösartigen Code.
  • node_modules: Der node_modules Ordner, in dem Module gespeichert werden, muss für jedes Projekt neu erstellt werden, auch wenn unterschiedliche Projekte dieselben Module verwenden, und kann sehr gross werden (siehe Bild unten)
  • CommonJS Module System (CJS): CJS ist im Vergleich zur neueren Art der Verwaltung von Modules, ES-Modules (ESM), unübersichtlich und veraltet. Da CJS in Node.js so weit verbreitet ist, lässt es sich nicht mehr entfernen.

node_modules

Figure: Quelle: https://github.com/denolib/awesome-deno/blob/main/resources/design-mistakes-in-node/design-mistakes-in-node.pdf

Deno’s Features

Schauen wir uns also mal die Top-Features von Deno an:

  • Batteries Included: Deno benötigt keine zusätzlichen Hilfsprogramme. Die meisten benötigten Tools wie Package Manager, Compiler, Code Formatter, Linter sind direkt in Deno enthalten.
  • Secure by Default: In Deno müssen Zugriffsberechtigungen, beispielsweise Netzwerkzugriff, Festplattenzugriff etc. explizit gegeben werden. Das verhindert, dass Programme, nur genau die Berechtigungen haben, die sie auch benötigen, ganz nach dem Prinzip der geringsten Privilegien.
  • TypeScript Unterstützung.
  • Geprüfte Standardmodule die auf keine externen Dependencies zurückgreifen.
  • ES Modules Unterstützung.
  • Packages/Modules sind dezentral: Es wird nur eine URL für den Import der entsprechenden Datei benötigt.
  • Top Level Await: await muss nicht mehr in eine async Funktion gewrapped werden, was für übersichtlicheren Code sorgt.
  • Browser API-Zugriff: Es werden keine zusätzlichen Packages benötigt, um beispielsweise fetch zu benutzen.

Wie wir sehen können besitzt Deno einige Features, die Entwicklern das Leben leichter machen und die Übersicht sowie Lesbarkeit von Code verbessern. Ryan Dahl’s Probleme mit Node.js bezogen sich hauptsächlich auf die Handhabung externer Dependencies, das macht Deno auf jeden Fall besser. Da Deno direkt mit vielen praktischen Tools geliefert wird, ist die Bezeichnung “Runtime” vielleicht etwas zu kurz gegriffen. Man könnte es schon eher als JavaScript/TypeScript Toolchain bezeichnen.

ES Modules

In Deno werden statt CommonJS Modules, ES Modules verwendet. Das Importieren von Packages sieht dann nicht mehr wie in Node.js so const package = require("package") aus, sondern so import package from 'package'. ES Modules haben folgende Vorteile:

  • Mit import lässt sich genau selektieren, welchen Teil einer Package man importiert haben möchte, was Speicher spart.
  • ES Modules werden nicht mehr wie bei CommonJS Modules synchron, sondern asynchron geladen.

Ein Import der assert.ts Package aus der Standart Library von Deno sieht beispielsweise so aus:

1
import { assertEquals } from "https://deno.land/std@0.158.0/testing/asserts.ts";

Permissions

Mithilfe von Permissions lässt sich einstellen, welche Rechte das Programm hat, das gerade ausgeführt wird. Diese Permissions werden durch verschiedene Flags gegeben. Wenn unser Programm beispielsweise auf das Internet zugreifen können soll, muss die --allow-net Flag mitgegeben werden. Geben wir keine Zugriffsberechtigung explizit mit, wird man von Deno automatisch für jede einzeln gefragt, ob man sie zulassen möchte.

Probieren wir mal aus, mit der Deno Version eines curl-Programmes die Seite https://example.com ohne Permissions aufzurufen:

Wir können jetzt natürlich einfach mit y bestätigen, aber wenn wir von Anfang an die Erlaubnis erteilen, kriegen wir keine Probleme und direkt den entsprechenden Header auch zurückgeliefert:

Tipp: Benutze die -A Flag, um alle Permissions zu erlauben. Dies kann bei der Entwicklung hilfreich sein, wenn du dich nicht immer mit den entsprechenden Permissions herumschlagen möchtest.

Top Level Await

await Funktionen müssen in Node.js immer in einer async Funktion gewrapped werden, was unübersichtlich werden kann:

1
2
3
4
const fakeData = async () = {
		const data = await fetch("https://example.com/movies.json");
		const result = await data.json();
};

Der äussere Wrapper kann dank Top Level Await in Deno nun weggelassen werden:

1
2
const data = await fetch("https://example.com/movies.json");
const result = await data.json();

Aufbau von Deno

Deno ist in Rust und JavaScript geschrieben. Im Grunde ist Deno einfach nur eine Ansammlung von Rust-Crates. Mit dieser Information im Hinterkopf, schauen wir uns doch die Architektur von Deno etwas genauer an.

Die Deno Runtime besteht aus folgenden Bausteinen:

  • JavaScript Engine: V8 (C++) und darauf aufbauende Schichten, namentlich rusty_v8 und deno_core, die die V8 für Rust benutzbar machen.
  • Event Loop: Tokio - Das Äquivalent zu libuv in Node.js.
  • Type Script Compiler: TSC + SWC
  • Code Caching und Analyse: “module_graph”.

JavaScript Engine

Rusty_v8 ist ein Rust Crate, welcher high-quality Bindings für die V8 zur Verfügung stellt. Dieser rusty_V8 Crate wird wiederum vom deno_core crate verwendet. deno_core ist ebenfalls ein low-level Crate, aber ein Level höher als rusty_V8. Wenn man deno_core verwendet, muss man also nicht direkt mit der V8 interagieren, sondern die V8 wird etwas abstrahiert, was einem das Leben einfacher macht.

deno_core

Der deno_core stellt aber nicht nur die JavaScript Runtime zur Verfügung, sondern ist auch für die Implementation von ES Modules zuständig. Deno_core ist auch für die Bereitstellung von “Ops” und “Resources” zuständig; dabei geht es um die Verbindung von high-level JavaScript Code mit low-level Rust code. Grundsätzlich kann man festhalten, dass sich deno_core ganz einfach um die Ausführung von JS Dateien kümmert. Es ist für alle Aufgaben zuständig, die nicht von Tokio oder der V8 übernommen werden.

Tokio

Tokio ist der für den Event Loop zuständige Rust Crate. Tokio ist ebenso wie das von Node.js verwendete libuv für asynchrone I/O zuständig. Für das Handling dieser asynchronen Prozesse werden in Rust sogenannte “Futures” verwendet, welche mit dem in JavaScript bekannten Konzept der “Promises” zu vergleichen sind.

module_graph

Der module_graph kümmert sich um das rekursive fetching und caching der Dependencies. Diese werden ähnlich wie bei Node.js gespeichert, aber eben nicht pro Projekt, sondern stehen Lokal für das ganze System zur Verfügung. Der module_graph kümmert sich aber nicht nur um das caching von externen Quellen, sondern auch um das caching von lokalen Quellen. Dies ist beispielsweise der Fall, wenn eine TypeScript Datei ausgeführt werden soll; diese muss erst zu JavaScript transpiliert werden, damit sie auf der V8 laufen kann, und wird ebenfalls gecached.

TypeScript Support

Zu Deno’s TypeScript Support ist zu erwähnen, dass Ryan Dahl in einem Podcast im Jahr 2021 erwähnt, dass der frühe TypeScript Support in Deno ein Fehler war. Dies begründet Dahl damit, dass Deno’s Hauptziel die Vereinigung von Server-Side JavaScript und Browser JavaScript ist - browser unterstützen TypeScript jedoch grundsätzlich nicht, was die Entwicklung von Deno und die erhaltung der Web-Kompatibilität stark verkompliziert hat.

Das Problematische an der Unterstützung von TypeScript out-of-the-box ist unter anderem auch der Fakt, dass der TypeScript Compiler (TSC) komplett synchron läuft, und somit langsam ist. Dieses Problem wurde gelöst, indem das gesamte heavy-lifting, also die Dependency Analysis, Transpilierung etc. von TypeScript Code auf Speedy Web Compiler (SWC), ein in Rust geschriebener TypeScript Compiler, ausgelagert wurde. Aktuell ist TSC nur noch für das Type Checking von TS Code zuständig.

Und die anderen?

  • dprint: Rust-basierter Code Formatter
  • deno_lint: Code Linter
  • deno_doc: Documentation Generator
  • Es gibt viele weitere Rust-Crates, die auf dieser Grafik nicht sichtbar sind.

deno-architecture

Figure: Deno Architektur

Deno vs. Node.js

Lassen sich Deno und Node.js gemeinsam verwenden?

Tatsächlich gibt es die Möglichkeit, beispielsweise NPM Packages auch in Deno zu verwenden. In vielen Fällen funktionieren diese wohl auch. Weitere Informationen dazu findest du hier. Bei meiner Recherche bin ich zudem auf dieses Cheatsheet hier gestossen, das den Übergang zwischen Node.js und Deno vereinfacht.

Ist Deno wirklich schneller als Node.js?

Mittlerweile ist Deno in den meisten Fällen gleich schnell oder schneller als Node, wenn es beispielsweise um die Anzahl Requests geht, die ein Server verarbeiten kann. Dazu habe ich hier einen interessanten Benchmark gefunden, der täglich viele Projekte in diesem Bereich miteinander vergleicht. Deno selbst trackt die Geschwindigkeit für jeden einzelnen Commit, was ein Zeichen dafür ist, dass diese Art von Optimierung für das Entwicklerteam einen hohen Stellenwert hat. Dennoch gibt es für spezifische Usecases wahrscheinlich optimalere, effizientere Lösungen als Deno.

Fazit - Ist Deno wirklich besser als Node?

Ganz persönlich; mich hat Deno positiv überrascht. Im Jahr 2020, als die erste Version von Deno rauskam, ging ein rechter Hype durch die JavaScript-Szene. Diese mediale Aufmerksamkeit hat sich zwar schnell wieder beruhigt, die Entwickler von Deno sind jedoch am Ball geblieben. Ich persönlich finde Deno aufgrund seiner Schlichtheit und dem Fakt, dass es direkt mit einigen praktischen Tools ausgeliefert wird, sehr attraktiv. Dies, kombiniert mit dem guten Gewissen, dass die Deno Standard Modules vom Core-Deno-Team geprüft werden und das Ökosystem rund um Deno allgemein mit viel weniger Dependencies auskommt, gibt einem ein gutes Gefühl.

Viele fragten sich, ob Deno in Zukunft Node.js ablösen könnte. Das kann kann ich mir vorstellen, vielleicht zu Beginn erst für simple Projekte. Die Value Proposition von Deno ist aber wohl nicht so stark, dass jetzt alle ihre Node.js Projekte aufgeben und zu Deno rennen.