esbuild, Teil 1: JavaScript-Bundling leicht gemacht

Als Hoffnungsträger unter den JavaScript-Bundlern gilt esbuild. Zu seinen Vorteilen zählen eine hohe Geschwindigkeit und eine leichte Erweiterbarkeit.

In Pocket speichern vorlesen Druckansicht 10 Kommentare lesen

(Bild: Shutterstock)

Lesezeit: 18 Min.
Von
  • Lars Gersmann
Inhaltsverzeichnis

Wer täglich mit der Entwicklung von Frontend-JavaScript und CSS zu tun hat, muss sich früher oder später mit JavaScript-Bundlern wie webpack, Parcel oder Rollup befassen. Bundler-Konfigurationen werden mit der Zeit oft komplex und kommen fast nie ohne Plug-ins von Drittanbietern aus, was auf lange Sicht die Pflege der Konfiguration und des Plug-in-Managements sehr aufwendig macht.

Zeit, sich mit esbuild auseinanderzusetzen: fast schon unverschämt schnell und so einfach erweiterbar, dass man durchaus auch ohne Plug-ins anderer Anbieter rasant zum Ziel kommt. Der Neuling esbuild hat sich zur Aufgabe gemacht, Bundling und Transpilation nicht nur einfacher umzusetzen als die Konkurrenz, sondern auch wesentlich performanter.

esbuild

Ob bei großen oder kleinen Web-Frontend-Projekten führt an der Nutzung eines JavaScript-Transpilers mittlerweile kein Weg mehr vorbei – und sei es nur, um JSX-Syntax (JavaScript Syntax Extension) in Vanilla-JavaScript umzuwandeln. Kommen dann noch Anforderungen für die Lauffähigkeit in älteren Browsern oder die Integration in eine bereits bestehende JavaScript-Umgebung hinzu, wird es nötig, sich mit der oft anspruchsvollen Konfiguration auseinanderzusetzen. Dazu kommt, dass viele Bundler auch als Build-Tools zu sehen sind beziehungsweise sich als solche ausgeben, was ihre Komplexität weiter erhöht.

Definition: Build-Tool, Transpiler oder Bundler
  • Build-Tools sind sprachunabhängige Werkzeuge zur Definition und Ausführung von einzelnen Build-Schritten, die untereinander Abhängigkeiten haben dürfen. Typische Vertreter sind Gulp, Grunt und make.
  • Transpiler sind sprachspezifische Werkzeuge, die eine Syntax in eine andere überführen können. In der Webentwicklung werden Transpiler wie webpack, Rollup, Sass oder esbuild häufig eingesetzt.
  • Bundler sind zielplattformspezifische Werkzeuge zum Erzeugen von plattformspezifisch ausführbaren Artefakten. Bei Bundlern für Webanwendungen verschwimmen die Grenzen: Bundler sind meist auch (gezwungenermaßen) Transpiler. Typische Vertreter sind webpack, Rollup und esbuild.

Build-Tools wie make verwalten Aufrufe an sprachspezifische Werkzeuge, etwa Compiler, und dienen in erster Linie der Definition einzelner Build-Schritte wie "Kompilieren" oder "Zip-Archiv bauen" und der Abhängigkeiten zwischen ihnen. So lässt sich definieren, dass der Schritt "Zip-Archiv bauen" als Vorbedingung den Schritt "Kompilieren" hat. In ausgewachsenen Build-Tools wie make lassen sich zusätzlich einzelne Build-Schritte als Abhängigkeiten von (Quell-)Dateien und Verzeichnissen angeben. Dadurch wird etwa der Schritt "Kompilieren" – und in der Folge auch der Schritt "Zip-Archiv bauen" – nur ausgeführt, wenn sich eine der Quelldateien geändert hat. Somit erfolgen nur die Build-Schritte, die notwendig sind.

Transpiler sind sprachspezifische Tools, die Quelltexte in eine andere Syntax überführen. Im Falle von JavaScript soll der Einsatz eines Transpilers oft sicherstellen, dass die transpilierten Quelldateien nur Sprachfeatures beinhalten, die die Zielsysteme oder Browser verstehen. Transpiler gibt es in vielen Programmiersprachen, allerdings ist die Begrifflichkeit "Transpiler" neuer als manches entsprechende Werkzeug. So ist der originale C-Präprozessor zwar schon uralt (erste Erwähnung 1972-73) aber letzten Endes ein Transpiler. Auch diverse CSS-Tools wie Sass oder Less sind Transpiler.

Viele Bundler wurden ursprünglich als reine Transpiler entwickelt, zum Beispiel Babel oder Browserify. Dabei entstand schnell der Wunsch, dass diese Tools doch auch "das bisschen Bundling" übernehmen könnten. Bundling umfasst das Erzeugen des endgültigen Softwareprodukts, das im Fall von webbasierter Software üblicherweise aus deploybaren JavaScript- und CSS-Dateien, einer Manifest-Datei und weiteren Assets wie Images und/oder Fonts bestehen kann.

Bundler sind meist plattformspezifische Tools, die das Programm für die Zielplattform individuell verpacken. Das Ergebnis kann eine Art komprimiertes Archiv sein, wie im Falle von JAR-Dateien für Java. Für Windows-Softwareentwickler stehen etwa InstallShield oder der Microsoft-MSI(X)-Bundler MakeAppx.exe zur Verfügung. Für browserbasierte Anwendungen besteht die Aufgabe des Bundlers im Zusammenfügen vieler einzelner JavaScript-/CSS-Quelldateien zu einigen wenigen großen. Der Bundler untersucht die Quelldateien auf Abhängigkeiten untereinander und produziert daraus ein monolithisches JavaScript- und CSS-Artefakt, das alle notwendigen Abhängigkeiten beinhaltet.

In der JavaScript-Welt lassen sich "Transpiler" und "Bundler" nicht klar unterscheiden. Das liegt daran, dass sich JavaScript im Gegensatz zu vielen anderen Programmiersprachen in sehr unterschiedlichen Zielumgebungen einsetzen lässt: sowohl im Browser als auch mittels Node.js außerhalb des Browsers. Diese Plattformen sind aus sicherheitstechnischen und historischen Gründen weit voneinander entfernt. So nutzt Node.js zum Referenzieren anderer JavaScript-Module den Common.js-Standard (const Foo = require('foo.js');), wogegen sich im Browser ausschließlich – erst innerhalb der letzten Jahre – der ECMAScript-Standard mittels import/export zum Laden weiterer JavaScript-Dateien durchgesetzt hat. Der JavaScript-Bundler ist dafür zuständig, die Quelldateien für die entsprechende Zielplattform umzuschreiben, damit diese ihn ausführen kann. Da JavaScript-Projekte der Übersichtlichkeit halber oftmals in sehr viele kleine JavaScript- und CSS-Dateien gesplittet sind, ist eine weitere Aufgabe des Bundlers, sie in wenige große Dateien zusammenzufassen, da das Nachladen vieler kleinerer Dateien im Browser die Performance der Webanwendung deutlich verschlechtern würde. Auch JavaScript-Bibliotheken speziell für Node.js werden oft gebundled, um die Ausführungszeiten einer Node.js-Anwendung zu beschleunigen.

Das Verwischen der Grenzen zwischen Bundler, Transpiler und Build-Tool hat einen entscheidenden Nachteil: All diese Features in einer Software erhöhen die Komplexität des Tools und damit den Aufwand für die Pflege und Nutzung der Software.

Eine typische Verwässerung, die sich aber leider nicht vermeiden lässt, ist etwa das Referenzieren von CSS-Dateien in JavaScript-Dateien in dieser Form: import "mycontrol.css";. Das findet sich in sehr vielen umfangreichen JavaScript-Frontend-Libraries.

Die ersten Transpiler wie Browserify kamen vor etwa zehn Jahren auf – geboren aus dem Wunsch, JavaScript-Code sowohl in Node.js als auch im Browser zu verwenden. Da dort zu dieser Zeit noch keine Möglichkeit bestand, Abhängigkeiten zwischen Modulen und exportierten Objekten aufzulösen, ließen sich npm-Module nicht ohne weiteres im Browser wiederverwenden. Transpiler sollten also Node.js-JavaScript-Code für Browser umschreiben, zu einer oder mehreren größeren JavaScript-Dateien verdichten und Modulabhängigkeiten auflösen.

Mit dem Aufkommen von React und dessen JSX-Erweiterung entstand die Notwendigkeit, daraus wieder regulären, von der JavaScript-Engine direkt ausführbaren Vanilla-JavaScript-Code zu erzeugen.

Ein auch heute noch als Platzhirsch geltender JavaScript-Transpiler ist Babel. Er lässt sich über Plug-ins sogar derart erweitern, dass in der Folge selbst proprietäre JavaScript-Sprachfeatures durch jedermann implementierbar und mittels Babel-Plug-ins in Standard-JavaScript-Code transpilierbar sind, der im Browser beziehungsweise Node.js ausgeführt werden kann. Allerdings besitzt Babel dadurch eine komplexe Konfiguration.

Die Dominanz von Googles JavaScript-Engine V8 und der dadurch aufgebaute Konkurrenzdruck gegenüber Mozilla Firefox und Apple Safari führte in den letzten Jahren zu einer beschleunigten Einführung neuer JavaScript-Sprachfeatures. Andere JavaScript-Engine-Hersteller sahen sich dadurch dazu gezwungen, mit neuen Sprachfeatures nachzuziehen. Dadurch geriet die Notwendigkeit des Transpilierens von brandneuen Sprachfeatures gegenüber dem Bundlen – also dem Zusammenführen von JavaScript-Code aus verschiedenen Modulen in einer Datei – in den Hintergrund. Einzig das Transpilieren von JSX-Syntax in JavaScript-Code ist nach wie vor ein wichtiges Feature von Transpilern, da JavaScript-Engines es nicht nativ unterstützen.

Die aktuellen Bundler beschäftigen sich somit hauptsächlich mit dem Bundlen von JavaScript-Code. Andere Dinge wie das Transpilieren von JSX-Syntax werden meist an weitere Tools wie Babel oder Sucrase delegiert und sind überwiegend nicht Bestandteil des eigentlichen Bundlers. Populäre Vertreter dieses Genres sind webpack, Parcel und Rollup.

Die native Unterstützung von ESM (ECMAScript Modules) im Browser, die sich durchgesetzt hat, vereinfacht dieses Konzept. Moderne Browser verstehen das import-Statement und sind dadurch in der Lage, portablen JavaScript-Code in ESM-Form selbstständig nachzuladen und auszuführen.

Damit erblickten die aktuellsten Vertreter der Bundler das Licht der Welt: Sie versuchen meist nicht, mit jedem JavaScript- oder Node.js-Feature kompatibel zu sein, sondern schneiden diese alten Zöpfe ab. So kann Vite.js das im Node.js-CJS-Format (CommonJS) distribuierte react-npm-Paket nicht verarbeiten. Stattdessen ist es nötig, eine in ESM transpilierte alternative Variante bereitzustellen. Das alles hat den triftigen Grund, die Turnaround-Zeiten bei der Entwicklung zu minimieren, denn schließlich sind Babel, webpack und Co. mittlerweile recht umfangreich und behäbig geworden.

Vertreter dieser modernsten Generation von JavaScript-Bundlern sind Vite.js und Snowpack. Beiden gemein ist, dass sie im Unterbau komplett auf ECMAScript Modules setzen und im Gegensatz zu webpack, Babel und Co. nur einmal nach ESM transpiliert werden und damit dem Browser direkt verfügbar gemacht werden können. Vite.js stammt von den Machern von Vue.js, lässt sich aber auch für React, Preact und einige weitere Frameworks verwenden. Vor allem das Hot Module Replacement (HMR) – also das Ersetzen von JavaScript-Code zur Laufzeit – hat diese Tools radikal beschleunigt. HMR ist eine ausgezeichnete Sache, die es vereinfacht gesagt erlaubt, eine JavaScript-Quellcodedatei im Editor zu ändern und die Änderung nach dem Speichern ohne Neuladen im Browser unmittelbar sichtbar zu machen.

Sowohl Snowpack als auch Vite.js verwenden unter der Haube zum Transpilieren und zum Bundlen esbuild – sie sind also Wrapper um esbuild. Dadurch laufen sie erheblich schneller als webpack, Rollup oder Parcel.

Oftmals geht es beim Transpilieren weniger um das Übersetzen moderner Sprachfeatures in Code, der auch in älteren JavaScript-Engines (ohne diese Sprachfeatures) ausführbar ist, sondern vielmehr um das Zusammenführen von Code aus vielen kleinen Dateien und die Integration in die JavaScript-Umgebung. Seit ECMAScript 6 ist dafür auch grammatikalisch ein standardisierter Weg definiert: import und export. Mithilfe dieser Statements kann eine JavaScript-Datei eine andere JavaScript-Datei referenzieren. Und nicht nur das – inzwischen ist selbst das Importieren/Nachladen von CSS- (import sheet from './styles.css' assert { type: 'css' };) und JSON-Dateien (import foo from './foo.json' assert { type: 'json' };) via import-Statement möglich. Das beherrschen moderne Browser nativ.

Man könnte auf den verheißungsvollen Gedanken kommen, ganz auf JavaScript-Transpiler zu verzichten. Wer allerdings schon einmal den node_modules-Ordner seines Projekts inspiziert hat, wird festgestellt haben, dass darin oft hunderte kleiner JavaScript-Dateien liegen. Theoretisch lassen sie sich im Browser laden, was aber aufgrund der geringen Geschwindigkeit unpraktikabel ist. Das liegt zum einen daran, dass viele Browser immer noch höchstens sechs gleichzeitige TCP/IP-Verbindungen zu einer Domain öffnen können. Dazu kommt, dass das Aufbauen einer TCP/IP-Verbindung oft länger dauert als das eigentliche Transportieren der JavaScript-Daten.

HTTPS/2 schafft ebenfalls keine Abhilfe. Es führte das Multiplexen von mehreren Requests in einer TCP/IP-Verbindung ein, wodurch – theoretisch – mehrere JavaScript-Dateien gleichzeitig über eine TCP/IP-Verbindung ausgeliefert werden können. Das ist allerdings nur ein Tropfen auf den heißen Stein, denn bei mehreren hundert solcher Dateien hilft es nur wenig, wenn anstatt sechs beispielsweise 24 Dateien gleichzeitig geladen werden können. Das Eindampfen vieler kleiner JavaScript-Dateien zu einer oder mehreren großen Dateien bleibt daher in der Praxis unerlässlich, um die Ladezeiten im Browser erträglich zu halten.

Dazu kommt, dass eine Applikation einen großen Teil des JavaScript-Codes in einer Datei oft überhaupt nicht nutzt, er aber im Modul implementiert ist. Ein Klassiker ist die Bibliothek lodash. Diese General Purpose Library bringt Dutzende von Funktionen mit, aber manche Applikationen verwenden nur eine oder zwei davon. Ein weiteres gutes Beispiel sind UI-Bibliotheken. Was, wenn die eigene Anwendung nur die Button-Komponente nutzt und die restlichen drei Dutzend Komponenten gar nicht benötigt?

Die Lösung für dieses Problem nennt sich Tree Shaking. Es sorgt dafür, dass nur der tatsächlich referenzierte Code in der erzeugten JavaScript-Datei landet, und kommt bei fast allen Transpilern in der einen oder anderen Form zum Einsatz.

Daher bleiben Transpiler auch in Zeiten von nativem import- und export-Browser-Support ein notwendiges Übel.

esbuild ist nicht als Build-Tool gedacht, sondern getreu der Unix-Philosophie ein Tool für einen Zweck: das Transpilieren von JavaScript. Es ist demnach kein Ersatz für make, Grunt, Gulp und Co., sondern wird von ihnen verwendet. Der Autor verwendet übrigens weder Gulp noch eines der unzähligen anderen JavaScript-spezifischen Build-Tools, sondern make. make bietet den Vorteil, dass man – wenn man einmal damit warm geworden ist – sein Wissen darüber jahrzehntelang pflegen und perfektionieren kann. Entwicklerinnen und Entwickler, die bisher auf Gulp, Grunt und Co. gesetzt haben, sollten sich make unbedingt anschauen. Es befreit nicht nur davon, alle fünf Jahre das nächstbeste Build-Tool erlernen zu müssen, sondern arbeitet zudem enorm effizient.

In den derzeit gehypten Bundlern Vite.js und Snowpack übernimmt esbuild die Rolle eines JavaScript-Transpilers, der von Haus aus JSX, TypeScript und selbst brandneue JavaScript-Sprachfeatures wie den Optional Chaining Operator und Nullish Coalescing Operator unterstützt. Der Einsatz von esbuild bietet einen großen Geschwindigkeitsvorteil gegenüber anderen Bundlern und Transpilern, wie die nachfolgende Grafik zeigt:

esbuild schneidet im Performance-Vergleich mit anderen Bundlern wie webpack deutlich besser ab.

(Bild: esbuild)

Neben der stark erhöhten Performance hat esbuild weitere Vorteile.

Die esbuild-Macher haben viel von den Problemen der webpack- und Babel-Transpilerentwicklung gelernt. So entschieden sie sich dafür, den JavaScript-Parser komplett in Go zu entwickeln, anstatt wie bei Babel auf in JavaScript geschriebene Parser-Erweiterungen für alle neuen Features zu setzen. In Go geschriebene Programme werden in nativen Maschinencode kompiliert, was die enorme Performance von esbuild gegenüber webpack und Co. erklärt. Da esbuild bereits alle JavaScript-Features in Go implementiert, entfällt die Konfiguration eines Plug-ins für jedes kleine Feature, wie es in webpack nötig wäre.

Nicht zuletzt bringt der monolithische (Go-)Ansatz einen weiteren Vorteil: Er ist einfach zu nutzen. Da im Monolith alles Nötige eingebacken ist, was zur JavaScript-Transpilation nur denkbar wäre, sind für die Transpilation keine zusätzlichen Erweiterungen zu installieren und konfigurieren.

Wer schon einmal Babel konfigurieren musste, weiß das monolithische Konzept von esbuild zu schätzen. "Welches Babel-Plug-in muss ich noch installieren, damit dieses oder jenes JavaScript-Feature unterstützt wird?" – esbuild-Nutzern ist dieses Problem fremd. "Monolithisch" ist heutzutage immer ein wenig negativ behaftet, doch unter Bereitstellung durchdacht designter und damit langfristig stabiler Schnittstellen ist der Ansatz im konkreten Anwendungsfall eher ein Vorteil.

Zur Erweiterbarkeit bringt esbuild eine Plug-in-Schnittstelle mit, über die sich sowohl mittels Go als auch JavaScript individuelle Funktionen hinzufügen lassen. Mittlerweile existieren dafür viele Plug-ins, die auf der esbuild-Homepage gelistet werden. Diese API werden wir uns im zweiten Teil der Artikelserie zunutze machen.

esbuild bietet für die Konfiguration der API verschiedene Möglichkeiten: die Kommandozeilen-Schnittstelle, die JavaScript- und die Go-API. Die erste Option entfällt für unsere Zwecke, da sie die Verwendung von Plug-ins (noch) nicht unterstützt. Die alternative JavaScript-Schnittstelle ist Nutzerinnen und Nutzern von Gulp, Grunt oder webpack nicht unbekannt. Dort kommt ebenfalls die JavaScript-API zur Anwendung. "Configuration as Code" ist an der Stelle ohnehin wesentlich praktikabler als eine hypothetische Kommandozeile, die unleserlich lang werden würde.

Die Verwendung existierender esbuild-Plug-ins kann oft mehr Probleme nach sich ziehen als das Plug-in löst. Zur Veranschaulichung eignet sich die Sass-Integration (Syntactically Awesome Stylesheets).

Für esbuild gibt es aktuell zwei Plug-ins, die Sass-CSS-imports implementieren. Man hat also die Qual der Wahl, welches Plug-in wohl das passende ist.

Bei genauerer Betrachtung fällt auf, dass in beiden Plug-ins das Durchreichen der Sass-Optionen entweder gar nicht oder nur teilweise umgesetzt ist. Außerdem neigen manche Plug-in-Autoren bei der Benennung von Plug-in-Optionen zu mehr Kreativität als notwendig, anstatt einfach die Parameter des durch das Plug-in gewrappten Tools eins zu eins an das Tool durchzureichen. Das erschwert die Nutzung dieser Plug-ins erheblich, da Anwender erst bei Sass nachschauen müssen, wie das Sass-Feature heißt, und dann in der Dokumentation des Plug-ins nachlesen müssen, wie der Autor den Parameter im Plug-in genannt hat.

Zum anderen, und das ist der gewichtigere Punkt, haben Entwicklerinnen und Entwickler keinen Einfluss mehr auf die Version des Sass-Compilers, da er als Dependency des Plug-ins enthalten ist. Im schlimmsten Fall entfällt daher die Chance, eine neuere Version des eigentlichen Tools – in diesem Fall des Sass-Compilers – zu verwenden. Stattdessen bleibt nur die Hoffnung, dass der Plug-in-Autor zeitnah eine neue Version des Plug-ins veröffentlicht.

Gerade in Bezug auf die Integration existierender Tools wie Sass ist es vorteilhafter, das selbst in der Hand zu haben.

Der Einsatz neuer Technologien wie ECMAScript Modules, moderner JavaScript-Bundler wie esbuild und klassischer Build-Tools wie make bringt viele Vorteile. Native JavaScript-Transpiler verringern die Turnaroundzeiten massiv. Das erworbene Wissen beispielsweise über make lässt sich viele Jahrzehnte verwenden – im Gegensatz zu alle paar Jahre wechselnden "Spezial-Build-Tools" wie Gulp oder Grunt oder gar der nicht vorgesehenen Verwendung von webpack als Build-Tool.

Wer Frontend-Projekte schon mit Bundlern wie webpack und Co. umgesetzt hat, weiß, dass sich früher oder später durch die von Plug-ins von Drittanbietern "weggewrappten" Tools oft Updateprobleme einstellen, was sich durch die Separation des Build-Prozesses vom Bundlen vermeiden lässt. Der Einsatz von Plug-ins anderer Anbieter ist zwar nicht immer vermeidbar, aber oft lohnt sich ein zweiter Blick. Für den Autor hat sich in seinen aktuellen Projekten gezeigt, dass Plug-ins oftmals gar nicht notwendig sind, wenn man den Bundler nur bundlen lässt, und ihn nicht auch als Build-Tool zweckentfremdet.

Der nächste Teil wird den konkreten Einsatz von esbuild zum Bundlen von JavaScript-Dateien zeigen, die sowohl JSX und ECMAScript Modules als auch Sass-CSS-Dateien verwenden.

Lars Gersmann
arbeitet bei der CM4all Gmbh, einer Tochter der IONOS/1&1-Gruppe als Open-Source-Entwickler. Er beschäftigt sich primär mit JavaScript/React, WordPress und Gutenberg. Am liebsten mit brandneuen NodeJS-/Browser-Technologien.

(mai)