DEV Community

Cover image for Angular & ESBuild: Dein Weg zum perfekten Tree Shaking 🌳🚀

Angular & ESBuild: Dein Weg zum perfekten Tree Shaking 🌳🚀

Wir kennen es alle. Wenn man täglich mit großen Angular-Anwendungen arbeitet, weiß man einfach, wie entscheidend Performance für eine gute UX (User Experience) ist. Ein wirklich großer Faktor hierfür ist nicht nur die effiziente Implementierung von Algorithmik oder ähnlichem sonder vor Allem auch die Größe der ausgelieferten JavaScript-Bundles, die aus unserem TypeScript-Code entstehen. Zum Verringern dieser gibt es verschiedene Möglichkeiten. Eine dieser Möglichkeiten möchte ich in diesem Beitrag etwas näher betrachten: Tree Shaking

Wir betrachten Tree Shaking hier vor allem ab Angular v17/v18 mit Standalone Components und ESBuild anstatt Webpack.

Es ist im Übrigen kein Problem, wenn Du noch nicht super viel Erfahrung hast - Tree Shaking ist für alle da. Ich versuche Dich schrittweise mitzunehmen.

Was ist Tree Shaking überhaupt?

Stell dir vor, dein Code ist ein großer, sehr verzweigter Baum. Jede Funktion, jede Klasse und jede Variable die du importierst ist ein Ast oder Blatt an diesem Baum.
Jetzt ist es jedoch so, dass wir sehr häufig Bibliotheken oder Module importieren, die viel Mehr an Funktionen bieten als wir tatsächlich in unseren Anwendungen nutzen.

Tree Shaking ist der Prozess, bei dem ein Bundler (wie früher Webpack oder jetzt ESBuild in Angular) erkennt, welche Teile unseres Codes tatsächlich von unserer Applikation genutzt werden. Alle ungenutzten "Äste" und "Blätter" werden vom Bundler aus den Artefakten, die wir später als Anwendung deployen, entfernt. Das Ergebnis ist ein kleineres, schlankeres und schneller ladbares Bundle, das der Browser des Users herunterladen muss.

Das Problem: Zu viel Code im Bundle

Bevor wir weiter ins Detail gehen, schauen wir uns ein kleines, aber alltägliches Szenario an: Wir nutzen eine Utility-Bibliothek, für diesen Beitrag die fiktive Bibliothek @angstitc/utils, welche diverse Funktionen bereitstellt:

// app.ts
import { formatEnergyAmount, calculateTax, validateEmail } from '@angstitc/utils'
@Component({
  selector: 'app-root',
  template: `
    <h1>Meine Anwendung</h1>
    <p>Preis: {{ formatEnergyAmount(123.45, 'kWh') }}</p>
    `
})
export class AppComponent {
  // ...
  formatEnergyAmount = formatEnergyAmount;
}

// node_modules/@angstitc/utilsindex.ts (vereinfacht)
export function formatEnergyAmount(amount: number, unit: string): string {
  // ... Logik zur Formatierung einer Energiemenge ...
  console.log('formatEnergyAmount aufgerufen!');
  return `${amount.toFixed(2)} ${unit}`;
}

export function calculateTax(amount: number, rate: number): number {
  // ... Logik zur Steuerberechnung ...
  console.log('calculateTax aufgerufen!');
  return amount * rate;
}

export function validateEmail(email: string): boolean {
  // ... Logik zur E-Mail-Validierung ...
  console.log('validateEmail aufgerufen!');
}  

Enter fullscreen mode Exit fullscreen mode

In diesem Szenario importieren wir formatEnergyAmount, calculateTax und validateEmail aus @angstitc/utils. Aber in unserer AppComponent nutzen wir nur formatEnergyAmount. Ohne effektives Tree Shaking würden calculateTax und validateEmail trotzdem in unserem finalen Bundle landen! Das ist unnötiger Balast.

Angular 17 und ESBuild: Die Traumhochzeit für Tree Shaking

Die gute Nachricht ist, dass Angular in Kombination mit ESBuild herborragend im Tree Shaking ist. ESBuild ist hierbei nicht nur schneller als Webpack es war, sondern auch noch effizienter.

Was macht ESBuild darin aber so gut? Die Antwort ist so unspektakulär wie simpel: Es wurde von Anfang an mit dem Fokus auf hohe Performance und Effizienz entwickelt. Hierzu nutzt es moderne JavaScript-Module (ESM - ECMAScript Modules), welche es sehr gut versteht und welche die Grundlage für effektives Tree Shaking bilden.

Wie funktioniert Tree Shaking (unter der Haube)

Damit Tree Shaking sich effektiv auswirkt, müssen einige Voraussetzungen erfüllt sein:

1. ESM-Module: ESM bilden die Basis. Die statischen import- und export-Statements können durch den Bundler vor Ausführung des Codes analysiert werden. Hierdurch wird klar, welche Funktionen und Codeabschnitte wirklich genutzt werden. Mit den dynamischen CommonJS-Imports (require()) wäre dies deutlich schwieriger zu erreichen.

2. Side-Effect-Free Code: Ein Modul gilt als Side-Effect-Free, wenn der Import des Moduls keine globalen Auswirkungen hat. So kann beispielsweise eine Bibliothek in ihrer package.json einen Eintrag in sideEffects haben, der dem Bundler dies klar kommuniziert. Dieser Eintrag kann neben boolschen-Werten auch ein Array von Dateipfaden sein (z.B. globale Stylesheets), anhand derer der Bundler weiß, welche Dateien er im Tree Shaking ignorieren muss.
In unserem oben gezeigten Beispiel erfüllen die genutzten Funktionen dieses Kriterium.

3. Minifier: Nach dem Tree Shaking kommt ein Minifier zum Einsatz. Dieser entfernt ungenutzten Code, der durch den Bundler markiert wurde und optimiert den gesamten Code durch z.B. Kürzung von Variablen-Namen und Unprettify (entfernen von Zeichen die nur der Formatierung dienen etc.).

Unser Beispiel in Aktion

Kehren wir zu unserem Beispiel zurück. Wenn wir unsere App mit der Prod-Configuration (sollte der Default sein) builden...

ng build
Enter fullscreen mode Exit fullscreen mode

...wird ESBuild unsere AppComponent analysieren. Es wird sehen, dass formatEnergyAmount verwendet wird, calculateTax und validateEmail jedoch nicht. Dadurch, dass alle drei Funktionen über einen separaten Export verfügen und die Bibliothek als Side-Effect-Free markiert ist (achtet bei euren Bibliotheken bitte darauf. Gute Util-Bibliotheken sind so gebaut), wird ESBuild die beiden ungenutzten Funktionen nicht in das auszuliefernde Bundle aufnehmen.
Das lässt sich überprüfen, indem man den dist-Folder ein wenig analysiert. Das Bundle wird kleiner sein und bei der Analyse der JS-Dateien (hierbei helfen Source Maps, da der Code minified wurde), stellen wir fest, dass calculateTax und validateEmail nicht im JavaScript-Artefact enthalten sind.
Das bedeutet: Selbst unused Imports wären nicht am Tree Shaking Türsteher vorbeigekommen.

Tipps, um Tree Shaking aktiv zu verbessern

Als Entwickler können wir aktiv dazu beitragen, das Tree Shaking in unseren Angular-Projekten zu verbessern:

  1. Immer spezifisch importieren:

    • Importiere nur was du brauchst
    • Gut: import { formatEnergyAmount } from '@angstitc/utils'
    • Vermeide wo möglich: import * as Utils from '@angstitc/utils'; Utils.formatEnergyAmount(...) - das erschwert die Statische analyse.
  2. Nicht jede Bibliothek schafft es in dein Projekt:
    Bevor du eine neue Bibliothek zu deinem Projekt hinzufügst, prüfe, ob sie moderne ESM-Module verwendet und Tree Shaking-freundlich ist. Gute Bibliotheken kennzeichnen dies oft in ihrer Dokumentation oder durch den sideEffects Eintrag in ihrer package.json.

  3. Side-Effects verstehen: Sei dir bewusst, dass Code, der globale Side-Effects hat (z.B. globale CSS-Dateien, die du direkt in TypeScript importierst und die keine Module sind), nicht einfach "weggeschüttelt" werden können. Das ist auch in Ordnung, da sie ja gebraucht werden. Nutze dies jedoch nur wo nötig.

  4. Immer Production-Builds ausliefern: Nur so werden Tree Shaking und Minifications angewandt.

Fazit

Tree Shaking ist keine magische Lösung für alle Performance-Probleme, aber es ist ein grundlegender und mächtiger Optimierungsschritt, der dir dabei hilft, die Größe deines JavaScript-Bundles signifikant zu reduzieren. Mit der nativen Integration von ESBuild ist das Tree Shaking in deinen Anwendungen effizienter und schneller als je zuvor.

Indem du die Prinzipien von Tree Shaking verstehst und bewusste Entscheidungen bei deinen Imports triffst, kannst du sicherstellen, dass deine Angular-Apps schlank, schnell und responsive bleiben. Deine Nutzer*innen werden es dir Danken - und deine Auftraggeber auch!

Top comments (0)