Stack update 2016: JavaScript Bundling mit Webpack 2 und Rollup

Stack update 2016: JavaScript Bundling mit Webpack 2 und Rollup

Der seit Aguinaga's popluärem Blogbeitrag “How it feels to learn JavaScript in 2016” vielzitierte Begriff "javascript fatigue" beschreibt den ständigen Druck JavaScript Werkzeuge und Bibliotheken neu zu lernen, da neuere, bessere, hippere Optionen verfügbar werden.

Ich glaube allgemein, dass dies nicht besonders problematisch ist. Dein Hauptfokus als Entwickler ist es, anständige Software mit einer aktzeptablen (nicht perfekten) Zusammenstellung von Technologien zu produzieren. Eine regelmässige Reevaluation der verwendeten Basistechnologien ist jedoch sinnvoll und doch irgendwie spannend. In diesem Blogbeitrag versuche ich unseren Webpack-zentrischen Buildstack mit aktuellen Optionen zu verbessern.

Statisches laden und "bundlen" / Packen von Modulen

Tl;dr: Programmiere in ES6, verwende den ES6 Modulsyntax und verwende Webpack 2 für komplexe Anwendungen, Rollup für Bibliotheken und einfacheres JavaScript.

Unsere alte Methode: Webpack 2

Das Verwalten von Abhänigkeiten ist eine komplexe Aufgabe die Bundler wie Browserify, Webpack oder Rollup lösen wollen. Eine einzige React-Komponente besteht potentiell aus einer Vielzahl individueller Pakete.

Das JavaScript-Engines noch kein Konzept von JavaScript Modulen haben (da das Laden von Modulen selbst auf Basis der noch nicht fertiggestellten WHATWG loader Spezifikation aufbaut) schmeißen Bundler alles in 1-2 JavaScript-Dateien die normal ausgeführt werden können. In großen JavaScript-Anwendungen würde das heißen, dass potentiell hunderte Module in einer Datei geladen werden, auch wenn diese im derzeitgen Kontext gar nicht benötigt werden.

Webpack stach hier immer durch seine Einsteigerfreundlichkeit und die Möglichkeit hervor, Webanwendungen mit mehreren Einstiegspunkten ("Entry points") zu realisieren und Code Splitting zu nutzen um gemeinsame Abhängikeiten zu schaffen die zwischengespeichert und wiederverwendet werden können.

Beispiel: Bandbreite sparen mit multiplen Einstiegspunkten

Als ein kleines Beispiel eine Webanwendung mit zwei Sektionen um zu zeigen wenn das nützlich ist:

  • Sektion A "http://beispiel.com/visualization/*": Beinhaltet eine 3D Animation, eingebettet in eine React-Komponente
  • Sektion B "http://beispiel.com/statistics/*": Verwendet eine Graphen-Bibliothek in einer React-Komponente

Unoptimiert: Traditionell würde man alles in ein singuläres Bundle / Datei packen:

  • Sektion A: bundle.js (React, Utility, Three.js, Chart.js) (800k)
  • Sektion B: bundle.js (React, Utility, Three.js, Chart.js) (800k) Das heißt: Wenn man eine Sektion besucht, wird das gesamte JavaScript heruntergeladen und initialisiert. An sich problemlos, aber ein riesiger initaler Dateidownload.

Optimiert: Mit mehreren Einstiegspunkten kann das intelligenter aufgeteilt werden:

  • Sektion A: base-bundle.js (React, Utility) (200k) + three-bundle.js (Three.js) (400k) = 600k
  • Sektion B: base-bundle.js (React, Utility) (200k) + chart-bundle.js (Chart.js) (200k) = 400k

Wenn man also nur Sektion B besucht würde man nur die hälfte des JavaScripts herunterladen müssen. In komplexeren Beispielen würde dies noch stärker bemerkbar machen.

Unsere aktualisierte Methode: Webpack 2 + Rollup

As Richard states: Rollup is by far the best choice for libraries, as it provides us with true ES6 interoperability and smaller bundles in complex scenarios. In complex applications webpack 2 still has the edge with advanced features like code splitting and while webpacks ES6 features don’t reach as deep as Rollup’s, it rarely makes a significant difference in production.

We now use both technologies: Rollup as our build tool of choice for library-like packages and scenarios where the javascript assets are handled very traditionally (for example in drupal themes) and webpack for our react and other client-side heavy apps. If code splitting support lands for rollup, this could change.

Der neue Modul-Builder Rollup verwendet ES6 Module nativ und hat zahlreiche Vorteile. Ein Vergleich mit den ES6-Fähigkeiten von Webpack 2 wird in diesem Kommentar vom Autor von Rollup, Richard Harris, gemacht. Die größten Vorteile sind:

  • Rollup verwendet den ES6 Modul Syntax nativ, was saubereren Syntax und bessere Zukunftssicherheit verspricht.
  • Es produziert kleinere Bundles
  • Es bietet die Möglichkeit einen eigenen ES6 Eingangspunkt in NPM-Paketen zu spezifizieren, was nicht zuletzt für die Wiederverwendung des eigenen Codes über NPM-Pakete sehr nützlich ist. Du musst es jedoch trotzdem "builden" da ein gemeinsames Set an erweiterten ES6 Features für die Interoperabilität notwendig ist.

Wie Richard feststellt: Rollup ist bei weitem die beste Wahl für Bibliotheken, da es richtige ES6 Interoperabilität und kleinere Bundles in komplexen Szenarien ermöglicht. In komplexen Anwendungen hat Webpack 2 durch fortgeschrittene Funktionen wie Code Splitting die Nase vorne und während Webpacks ES6-Funktionen nicht so tiefgreifend sind wie die von Rollup macht es selten einen signifikanten Unterschied in Production.

Wir verwenden nun beide Technologien: Rollup als unser Build-Tool der Wahl für Pakete die wir als Bibliotheken nutzen und Szenarien wo JavaScript-Assets traditionell verwendet werden (beispielsweise in Drupal Themes und Modulen) und Webpack für unsere React- und clientseitigen Anwendungen. Wenn Unterstützung für Code Splitting auch in Rollup ergänzt wird, könnte sich das jedoch ändern.

Dynamisches laden von Modulen

Tl;dr: Verwende Webpack 2 und dessen “System.import”. Du brauchst in einfacheren Anwendungen vermutlich keine dynamischen Imports.

In einer perfekten Welt würden alle Pakete in ES6 syntax bereitstehen, Browser würden diese dynamisch laden und auflösen und wir könnten komplett auf Rollup verzichten. Betrachtet man jedoch den Zustand der Implementierungen liegt diese Vision noch in weiter Ferne.

Zwischenzeitlich könnte man SystemJS verwenden um es zu ermöglichen ES6-Module dynamisch clientseitig zu laden, ähnlich dessen was der Browser machen würde. Die Bibliothek hat ca. 19kb gzipped. Das verwendete JavaScript muss in das eigene SystemJS Modul-Format transpiliert werden. Es wird derzeit diskutiert eine Integrationsschicht für die neue Paketverwaltung Yarn bereitzustellen, was das Management von Paketen für SystemJS um einiges erleichtern würde.
(Lese nach warum wir Yarn einsetzen)

Webpack 2 bietet uns asynchrone Unterstützung für Assets mit dem zukunftsorientierten "System.import" syntax, welcher sehr bequem genutzt weren kann, beispielsweise mit react-router. Aber Achtung: Dateien die mittels System.import inkludiert wurden können nicht mittels "tree-shaking" von unnötigen Abhänigkeiten befreit werden, darum wird jede Abhänigkeit eingebunden. Da alle importierten Dateien laut ES6 Modulspezifikation statisch analysierbar sein müssen ist es nicht möglich diese auf Basis von Runtime-Werten zu importieren (also bsp. nicht als Teil von if-else Konstrukten).

Fazit

Da alle Paketformate und Build-Tools das gleiche Problem von JavaScript Interoperabilität lösen wollen, ist die fortschreitende Verbreitung des ES6 Modul-Syntax in der JavaScript Community eine große Erleichterung. Neben den erweiterten Funktionen von Webpack ist Rollup ein vielversprechende Lösung und besonders gegenüber Tools wie Browserify eine Verbesserung.

Im Kontext von asynchronen Ladevorgängen und multiplen Einstiegspunkten sollte man immer die mögliche Ersparnis in bezug auf Bandbreite im Vorhinein abschätzen. Sogar in großen Anwendungen kann die Bedeutung von asynchronem Laden von Modulen oder Code splitting minimal oder die Ersparnis sogar negativ sein. Sich 5 Minuten Zeit nehmen und ein PNG zu optimieren spart oft mehr Bandbreite (und heiße Tränen der Verzweiflung) als stundenlang sein JavaScript Bundle zu optimieren (nicht, dass man das nicht trotzdem machen könnte).

Für allgemeine Hinweise und Fragen, freue ich mich von Dir auf Twitter zu hören.