
⚡ Wie man externes JavaScript lädt, ohne die Seite zu blockieren
Wenn der Browser auf ein <script>-Tag ohne Attribute trifft, lässt er alles fallen. Der Seitenaufbau kommt vollständig zum Erliegen, bis das Skript geladen und ausgeführt ist. Bei einer langsamen 4G-Verbindung sind das 2-3 Sekunden weißer Bildschirm.
Der Nutzer ist da bereits zur Konkurrenz abgewandert. Core Web Vitals verzeichnen einen verfehlten LCP, Google stuft die Seite in den Suchergebnissen herab, und Sie verlieren Traffic und Conversions. Dabei lässt sich das Problem mit drei Zeilen lösen, wenn man weiß, wo man ansetzen muss.
Nachfolgend eine funktionierende Methode, um externes JavaScript ohne Blockierung zu laden. Vom klassischen Zwei-Dateien-Ansatz über modernes async/defer bis hin zu dynamischem import(). Mit erprobtem Code zum Kopieren und Einfügen.
💡 Kurzüberblick:
- Das Problem verstehen: wie ein gewöhnliches
<script>das HTML-Parsing blockiert und die Ladegeschwindigkeit ruiniert - Den klassischen Ansatz beherrschen: ein winziger Loader (≤300 Byte) zieht dynamisch die Haupt-JS-Datei nach
- Die nativen Attribute
asyncunddeferkennenlernen: wann welches zum Einsatz kommt - Dynamisches
import()für das Laden von Modulen bei Bedarf erkunden - Mit einer Vergleichstabelle eine Strategie für Ihr Projekt wählen
Warum JavaScript das Rendering blockiert
Wenn der HTML-Parser auf <script src="app.js"> stößt, tut er genau drei Dinge: Er stoppt das Parsen des Dokuments, lädt die Datei herunter und führt sie aus. Erst danach setzt er den DOM-Aufbau fort.
Der Grund ist architektonischer Natur. Ein Skript kann document.write() enthalten, das HTML im laufenden Betrieb verändert. Der Browser weiß vorab nicht, ob ein solcher Aufruf vorkommt, und geht auf Nummer sicher, indem er auf vollständiges Laden und Ausführen wartet. Das Ergebnis: Selbst ein leichtgewichtiges 5-KB-Skript kostet allein durch einen einzigen Netzwerk-Roundtrip mehrere hundert Millisekunden First Contentful Paint.
Das Problem ist nicht neu. Bereits 2009 beschrieb Nicholas Zakas eine Technik für dynamisches, nicht blockierendes JavaScript-Laden, und sie funktioniert bis heute, wenn auch mit Anpassungen an moderne APIs. Mit dem Aufkommen von async, defer und ES-Modulen steht Entwicklern inzwischen ein ganzes Werkzeugset zur Verfügung. Sehen wir uns jedes einzelne an.
Klassischer Ansatz: zwei Dateien und dynamisches Laden
Die Idee ist einfach. Statt das gesamte JS in eine Datei zu packen und per <script src="..."> an die Seite zu binden, teilt man den Code in zwei Teile:
- Einen winzigen Loader (200-300 Byte nach Komprimierung)
- Die Hauptdatei mit der Anwendungslogik
Der Loader wird inline am Ende der Seite eingefügt, direkt vor </body>. Er erzeugt ein <script> programmatisch und fügt es ins DOM ein; ein solcher Tag blockiert das Parsen nicht mehr, weil er außerhalb des Hauptdokumentflusses erscheint. Sobald die Hauptdatei geladen ist, läuft die Initialisierung.
Moderne Version der Funktion in reinem JS ohne IE-Abwärtskompatibilität:
1 function loadScript(url) { 2 return new Promise((resolve, reject) => { 3 const script = document.createElement('script'); 4 script.src = url; 5 script.onload = resolve; 6 script.onerror = reject; 7 document.head.appendChild(script); 8 }); 9 }
Neun Zeilen. Keine readyState-Prüfungen, keine Verzweigungen für alte IE-Versionen, keine Callback-Pyramide. Einfach eine Funktion, die ein Promise zurückgibt, bequem kombinierbar mit async/await.
Die Verwendung auf der Seite sieht so aus (Code am Ende, vor dem schließenden </body>):
1 <script> 2 function loadScript(url) { 3 return new Promise((resolve, reject) => { 4 const script = document.createElement('script'); 5 script.src = url; 6 script.onload = resolve; 7 script.onerror = reject; 8 document.head.appendChild(script); 9 }); 10 } 11 12 loadScript('/js/app.js').then(() => { 13 // Initialize after main file loads 14 App.init(); 15 }); 16 </script>
Das erste Skript (inline) ist der Loader. Es wird sofort geparst und ausgeführt, weil es weniger als 300 Byte groß ist. Das zweite Skript (app.js) lädt asynchron und stört das Rendering nicht.
Was tun bei mehr als zwei Dateien? Führen Sie sie während des Builds zusammen. Moderne Bundler wie Vite und Webpack erledigen das automatisch: Tree-Shaking, Code-Splitting, Minifizierung in einem Durchlauf. Die Ladereihenfolge von einem Dutzend Dateien manuell zu verwalten, ist ein sicherer Weg zu Race Conditions und Fehlern.
Async und defer: natives Entblocken
HTML5 hat uns zwei Attribute beschert, die das Problem ohne eine einzige Zeile JavaScript lösen:
1 <script async src="analytics.js"></script> 2 <script defer src="app.js"></script>
Beide laden die Datei parallel zum HTML-Parsing. Der Unterschied liegt im Ausführungszeitpunkt:
Attribut | Laden | Ausführung | Reihenfolge |
|---|---|---|---|
| Parallel zum Parsing | Sofort nach dem Laden | Nicht garantiert |
| Parallel zum Parsing | Nach vollständigem HTML-Parsing | Garantiert (wie im Dokument) |
Faustregel:
asyncfür unabhängige Skripte: Analytics, Werbung, Zähler. Sie benötigen kein DOM, die Reihenfolge ist ihnen egal.deferfür die Hauptanwendung: DOM-Manipulation, Interface-Initialisierung. Das Skript wartet auf die Seitenbereitschaft und führt in der korrekten Reihenfolge aus.
In der Praxis ist die Kombination einfach: Setzen Sie defer auf alle Skripte im <head>, und sie verhalten sich, als stünden sie am Seitenende, laden aber früher. Keine Magie, nur der Browser-Scheduler.
Und ja, man kann es mit dynamischem Laden kombinieren. Laden Sie zum Beispiel den Anwendungskern per <script defer> und ziehen Sie schwere Widgets dynamisch per loadScript() erst dann nach, wenn sie tatsächlich gebraucht werden.
Dynamisches import(): Module bei Bedarf
ES2020 brachte dynamisches import(), einen nativen Weg, ein Modul asynchron zu laden, ohne Bundler und ohne Zusatzfunktionen:
1 // Loads only when user clicked 2 button.addEventListener('click', async () => { 3 const { heavyChart } = await import('./chart-component.js'); 4 heavyChart.render(); 5 });
Der import()-Aufruf gibt ein Promise zurück. Das Modul lädt im Hintergrund, das Parsen wird nicht blockiert, die Seite bleibt reaktionsfähig. Code innerhalb des Moduls läuft im Strict Mode und in einem eigenen Gültigkeitsbereich, Namenskonflikte sind ausgeschlossen.
Das ist das ideale Werkzeug für Code-Splitting ohne Bundler. Schwere Komponenten (Diagramme, Editoren, Karten) werden in separate Dateien ausgelagert und bei der ersten Interaktion geladen. Ein Nutzer, der nie ein Diagramm öffnet, bezahlt es nicht mit Traffic und Ladezeit.
Vergleich der Ansätze
Jede Methode hat ihre Nische. Um nicht zu raten, haben wir die Eigenschaften in einer Tabelle zusammengestellt:
Ansatz | Blockiert Rendering | Benötigt JS | Ausführungsreihenfolge | Für welche Skripte |
|---|---|---|---|---|
| Ja | Nein | Garantiert | Nur wenn unvermeidbar |
Dynamisches | Nein | Ja | Über | Konditionales Laden, schwere Abhängigkeiten |
| Nein | Nein | Nicht garantiert | Analytics, Werbung, Zähler |
| Nein | Nein | Garantiert | Hauptanwendung, DOM-Manipulation |
| Nein | Ja (ES-Modul) | Über | Code-Splitting, On-Demand-Komponenten |
Wichtigste Erkenntnis: Fixieren Sie sich nicht auf eine Methode. Ein typisches Produktionssetup nutzt zwei oder drei gleichzeitig: defer für den Kern, async für Metriken, dynamisches import() für schwere Komponenten.
Kurzes Demo-Video zum Thema, Aufschlüsselung von async und defer mit Visualisierung der Ladezeitleiste:
⁉️🤔 Häufig gestellte Fragen
Wie unterscheiden sich async und defer in der Praxis?
Beide blockieren das Parsen während des Ladens nicht. Aber
asyncführt das Skript sofort nach dem Laden der Datei aus, selbst wenn das HTML noch nicht vollständig geparst ist, und ohne Reihenfolgegarantie.deferwartet immer auf die vollständige DOM-Bereitschaft und erhält die Skript-Reihenfolge wie im HTML. Für Hauptanwendungscode nutzen Siedefer, für isolierte Zählerasync.
Kann man dynamisches Laden mit defer kombinieren?
Ja, das ist ein gängiges Szenario. Der Anwendungskern lädt mit
deferim<head>, er initialisiert das Interface. Schwere oder selten genutzte Module werden bei Benutzerinteraktion per dynamischemloadScript()oderimport()nachgezogen. So erhalten Sie sowohl einen schnellen Start als auch das verzögerte Laden von Sekundärcode.
Was sollte ich für eine WordPress-Seite verwenden?
WordPress fügt
deferoderasyncautomatisch überwp_enqueue_script()hinzu, wenn Sie das entsprechende Argument im fünften Parameter übergeben:wp_enqueue_script('my-script', $url, [], null, ['strategy' => 'defer']). Für Drittanbieter-Skripte (Google Analytics, Werbung) ist der einfachste Weg dasasync-Attribut. Komplexe interaktive Blöcke (Rechner, Filter) sollten in dynamischesimport()innerhalb eines benutzerdefinierten Moduls ausgelagert werden.
Funktioniert das mit Drittanbieter-Skripten wie Google Analytics?
Ja. Der GA4-Tag
gtag.jslädt standardmäßig mitasyncund blockiert die Seite daher nicht. Für andere Drittanbieter-Dienste prüfen Sie die Dokumentation: Wenn das Skript kein fertiges DOM benötigt und nicht von der Ladereihenfolge abhängt, können Sie bedenkenlosasyncverwenden. Benötigt es das DOM, nutzen Siedeferoder dynamisches Laden mit einem Callback.
Wie überprüfe ich, dass ein Skript die Seite wirklich nicht blockiert?
Öffnen Sie Chrome DevTools → Performance → Aufzeichnen → Seite neu laden. Suchen Sie auf der Zeitleiste nach gelben „Scripting"-Blöcken vor dem grünen „First Contentful Paint". Wenn ein Skript mit
deferoderasyncgeladen wird, liegt seine Ausführung nach dem FCP. Lighthouse zeigt im Modus „Performance" die Empfehlung „Render-blockierende Ressourcen entfernen"; in dieser Liste sollten keine Skripte auftauchen.
Lohnt es sich, den Ladeansatz jetzt umzustellen
Wenn Ihre Skripte noch ohne Attribute im <head> hängen, verlieren Sie Suchrankings und verärgern Nutzer. Das ist keine Hypothese, Lighthouse und PageSpeed Insights zeigen das Problem in den ersten Zeilen des Berichts rot an.
Schnellstart: Gehen Sie die <script>-Tags in Ihrem Template durch, fügen Sie defer für Hauptcode und async für Metriken hinzu. Das dauert fünf Minuten, und der LCP kann sich um 300-500 ms verbessern. Als Nächstes dynamisches import() für schwere Komponenten, wenn Sie zum Refactoring kommen.
Belassen Sie es bei einer Lademethode, <script defer> im <head>, und die Seite lädt ohne sichtbare Verzögerungen. Testen Sie es noch heute an Ihrem Projekt.



