JavaScript Polyfills und Transpiler
Wie modernes JavaScript auf älteren Engines läuft: Transpiler wie Babel für neue Syntax und Polyfills wie core-js für fehlende Built-in-Methoden.
JavaScript erhält jedes Jahr neue Funktionen. Jede jährliche Version — ES2015 (ES6), ES2020, ES2022 und so weiter — bringt neue Syntax und neue Built-in-Methoden. Das Problem ist, dass Ihr Code nicht in einer fixen Engine läuft: Er läuft in dem Browser oder der Laufzeitumgebung, die Ihre Besucher gerade verwenden, und einige davon sind jahrelang veraltet. Code, der in der neuesten Chrome-Version einwandfrei funktioniert, kann in einer älteren einen SyntaxError auslösen oder still fehlschlagen, weil eine Methode wie Array.prototype.includes schlicht nicht existiert.
Es gibt zwei separate Lücken zu schließen, und dafür braucht man zwei verschiedene Werkzeuge. Neue Syntax, die eine alte Engine nicht einmal parsen kann, benötigt einen Transpiler. Neue Built-in APIs, die eine alte Engine nie ausgeliefert hat, benötigen ein Polyfill. Dieses Kapitel erklärt beide, zeigt ihre Unterschiede und beschreibt, wie sie in eine moderne Build-Toolchain passen.
Die zwei Lücken: Syntax vs. APIs
Bevor man zu einem Werkzeug greift, hilft es zu verstehen, warum ein Werkzeug allein nicht ausreicht.
- Syntax ist die Grammatik der Sprache — Arrow-Funktionen,
class, Optional Chaining (?.), der Nullish-Coalescing-Operator (??), Template-Literale. Wenn eine Engine die Grammatik nicht versteht, schlägt das Parsen des Skripts fehl und nichts wird ausgeführt. Das lässt sich zur Laufzeit nicht beheben, weil die Datei abgelehnt wird, bevor irgendein Code ausgeführt wird. - APIs sind die Built-in-Funktionen und -Objekte, die während der Ausführung Ihres Codes verfügbar sind —
Promise,Array.prototype.includes,String.prototype.padStart,Object.fromEntries,fetch. Das sind nur Werte, die auf globalen Objekten und Prototypen liegen. Wenn eines fehlt, können Sie es selbst hinzufügen, bevor Ihr Code es verwendet.
Diese Aufteilung ist die ganze Geschichte: die Grammatik vorab umschreiben oder die fehlenden Werte zur Laufzeit bereitstellen.
Transpiler: Neue Syntax in alte Syntax
Ein Transpiler (auch Quell-zu-Quell-Compiler genannt) liest Ihr modernes JavaScript und schreibt es in gleichwertiges älteres JavaScript um, das mehr Engines verstehen. Der bekannteste Transpiler ist Babel. Dies geschieht zur Build-Zeit — bevor Ihr Code jemals ausgeliefert wird — sodass der Browser nur Syntax erhält, die er parsen kann.
Hier ist ein kleines Vorher-Nachher. Sie schreiben eine moderne Arrow-Funktion:
// Source — modern syntax
const double = x => x * 2;Ein Transpiler, der auf ältere Engines abzielt, schreibt sie in einen klassischen Funktionsausdruck um:
// Output — down-leveled to ES5
var double = function (x) {
return x * 2;
};Das Verhalten ist identisch; nur die Grammatik hat sich geändert. Babel macht dasselbe für class-Deklarationen, Destructuring, Default-Parameter, Optional Chaining und mehr. Zum Beispiel wird user?.address?.city in eine Reihe von &&-Prüfungen umgewandelt, die ältere Engines problemlos verarbeiten.
@babel/preset-env und browserslist
Man konfiguriert selten jede Funktion einzeln. Stattdessen verwendet man @babel/preset-env, ein Preset, das entscheidet, welche Transformationen basierend auf den Umgebungen angewendet werden, die man unterstützen möchte. Diese Umgebungen werden mit einer browserslist-Abfrage deklariert — eine kurze, gemeinsam genutzte Möglichkeit, Zielbrowser zu beschreiben:
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"not dead"
]
}Mit dieser Liste transpiliert @babel/preset-env nur das, was diese Browser tatsächlich nicht unterstützen. Schränkt man die Liste auf moderne Browser ein, wird fast nichts heruntergestuft; erweitert man sie auf veraltete, findet weit mehr Transformation statt. Der Kerngedanke: Ein Transpiler ist ein Build-Schritt, und browserslist gibt vor, wie viel Arbeit er leisten soll.
Polyfills: Fehlende APIs zur Laufzeit bereitstellen
Ein Polyfill ist ein Codestück, das ein fehlendes Built-in hinzufügt, damit eine alte Engine die API zur Laufzeit erhält. Ein Transpiler kann ?.-Syntax umschreiben, aber er kann kein Promise-Objekt herbeizaubern, das die Engine nie ausgeliefert hat — das ist ein Job für ein Polyfill. Die am häufigsten verwendete Polyfill-Bibliothek ist core-js, die Implementierungen für Promise, Array.from, Object.fromEntries, String.prototype.padStart und Hunderte weitere bereitstellt.
Man kann auch ein kleines Polyfill von Hand schreiben. Das wesentliche Muster ist eine Feature-Detection-Guard — eine if-Prüfung, die Ihre Version nur installiert, wenn die native nicht vorhanden ist:
if (!String.prototype.padStart) {
String.prototype.padStart = function (targetLength, padString) {
targetLength = Math.floor(targetLength) || 0;
if (targetLength < this.length) {
return String(this);
}
padString = padString ? String(padString) : ' ';
let pad = '';
const len = targetLength - this.length;
let i = 0;
while (pad.length < len) {
if (!padString[i]) {
i = 0;
}
pad += padString[i];
i++;
}
return pad + String(this).slice(0);
};
}Die Zeile if (!String.prototype.padStart) ist das Entscheidende. Ohne sie würde man die native Implementierung der Engine jedes Mal überschreiben — schnellen, gut getesteten Built-in-Code durch eigenen ersetzen. Die Guard besagt „nur eingreifen, wenn die Funktion wirklich fehlt", sodass moderne Engines ihre optimierte Version behalten und nur alte Engines auf Ihre zurückfallen.
Das folgende Beispiel erkennt und verwendet padStart genauso wie ein Polyfill es tun würde. In einem modernen Browser läuft die native Methode; in einem veralteten hätte die obige Guard-Fallback-Implementierung sie zuvor bereitgestellt.
Das Modifizieren eingebauter Prototypen (wie String.prototype) sollten nur Polyfills tun, und nur hinter einer Feature-Detection-Guard. Im eigenen Anwendungscode sollte man das Hinzufügen von Methoden zu nativen Prototypen vermeiden — es kann zu Konflikten mit anderen Bibliotheken und zukünftigen Sprachfunktionen führen.
Transpiler vs. Polyfill auf einen Blick
Die beiden Werkzeuge sind leicht zu verwechseln, weil beide zur Unterstützung älterer Engines existieren. Dieser Vergleich hält sie auseinander:
| Aspekt | Transpiler (z. B. Babel) | Polyfill (z. B. core-js) |
|---|---|---|
| Behebt | Neue Syntax, die die Engine nicht parsen kann | Fehlende Built-in APIs |
| Zeitpunkt | Build-Zeit (vor der Auslieferung) | Laufzeit (im Browser) |
| Beispieleingabe | ?., ??, Arrow-Funktionen, class | Promise, fetch, Array.prototype.includes |
| Funktionsweise | Schreibt Code in ältere Grammatik um | Fügt die fehlende Funktion/das fehlende Objekt hinzu |
| Kann die andere Lücke schließen? | Nein — kann keine fehlenden APIs hinzufügen | Nein — kann nicht-parsierbare Syntax nicht beheben |
Eine einfache Faustregel: Wenn eine Engine Ihren Code nicht lesen kann, brauchen Sie einen Transpiler; wenn sie ihn lesen kann, aber eine Funktion undefined ist, brauchen Sie ein Polyfill. Die meisten realen Projekte verwenden beides gleichzeitig.
Einbindung in eine moderne Toolchain
In der Praxis führt man diese Werkzeuge nicht von Hand aus. Ein Bundler oder Build-Tool — wie Vite, Webpack oder esbuild — steuert sie für Sie. Eine typische Einrichtung funktioniert so:
- Man deklariert seine Zielumgebungen einmal in
browserslist. - Das Build-Tool führt Babel mit
@babel/preset-envaus, das nur die Syntax herunterstuft, die den Zielen fehlt. - Die gleiche Konfiguration injiziert
core-js-Polyfills für die APIs, die diesen Zielen fehlen — und mituseBuiltIns: 'usage'nur diejenigen, auf die der Code tatsächlich verweist.
Das Ergebnis ist ein Bundle, das auf das echte Publikum zugeschnitten ist: Nichts wird transformiert oder polyfilled, was die Browser der Besucher bereits unterstützen.
Die heutigen Evergreen-Browser — Chrome, Edge, Firefox und Safari — aktualisieren sich automatisch und unterstützen bereits die große Mehrheit der modernen ES6 und neueren Funktionen. Umfangreiches Transpilieren und breites Polyfilling sind weitaus weniger notwendig als früher. Setzen Sie ein realistisches browserslist-Ziel für Ihr Publikum und lassen Sie die Toolchain nur das herunterstufen und polyfillen, was wirklich benötigt wird.
Es gibt echte Kosten, diesen Rat zu ignorieren. Übermäßiges Polyfilling bläht das Bundle mit Code auf, den jeder moderne Besucher herunterlädt, parst und ungenutzt wegwirft. Zu aggressives Herunterstufen produziert außerdem größere, langsamere Ausgaben. Das Ziel ist nicht „alles unterstützen", sondern „unterstützen, was die Benutzer tatsächlich verwenden". Um nachzuvollziehen, welche Funktionen ein bestimmter Browser unterstützt, ist das Kapitel über DOM und Browser-Kompatibilität ein nützlicher Begleiter.