JavaScript ist aus modernen Websites nicht mehr wegzudenken — aber es ist auch einer der häufigsten Gründe, warum Websites bei Google schlechter ranken als sie sollten. Zu viel JavaScript, falsch geladen oder schlecht optimiert, blockiert das Rendering, verschlechtert die Core Web Vitals und bremst Google beim Crawlen deiner Seiten aus.
In diesem Artikel zeige ich dir konkret, wie du JavaScript so optimierst, dass dein SEO-Ranking davon profitiert. Du lernst den Unterschied zwischen defer und async, wie du Lazy Loading richtig einsetzt und was Code Splitting für deine Ladezeit bedeutet.
Warum JavaScript ein SEO-Problem sein kann
Google crawlt und indexiert Websites mit dem Googlebot. Dieser Crawler versteht zwar JavaScript — aber nicht sofort. Es gibt zwei Phasen beim Google-Crawling:
- Erste Indexierung: Googlebot lädt die HTML-Datei und indexiert sofort sichtbare Inhalte
- Zweite Indexierung (Rendering): Googlebot rendert JavaScript — aber mit einer Verzögerung von Stunden bis Tagen
Wenn dein wichtiger Content erst durch JavaScript geladen wird, kann es sein, dass Google ihn erst Tage später sieht. Noch problematischer: Wenn JavaScript das initiale Rendering blockiert, werden deine Ladezeiten schlechter — und damit auch dein Ranking.
Die drei JavaScript-Probleme, die SEO am meisten schaden
- Render-Blocking Scripts: JavaScript das den Browser blockiert, bevor er Inhalte anzeigt → schlechterer LCP
- Zu großes JavaScript-Bundle: Ein 500 KB+ Bundle verzögert den First Contentful Paint deutlich
- Content nur via JavaScript: Texte, H1, Meta-Tags die erst nach JS-Ausführung erscheinen → schlechtere Indexierung
defer vs. async: Der Unterschied erklärt
Wenn du ein <script>-Tag ohne Attribute verwendest, blockiert der Browser das gesamte HTML-Parsing, bis das Script geladen und ausgeführt ist. Das ist das klassische Render-Blocking-Problem. Mehr dazu erkläre ich im Artikel über Render-Blocking-Ressourcen.
async — Parallel laden, sofort ausführen
Das async-Attribut lädt das Script parallel zum HTML-Parsing herunter. Sobald der Download abgeschlossen ist, pausiert der Browser das HTML-Parsing und führt das Script aus.
<!-- Script wird parallel geladen, aber unterbricht das Parsing bei Ausführung -->
<script async src="analytics.js"></script>
Wann verwenden? Für unabhängige Scripts wie Analytics (Google Analytics, Matomo) oder Tracking-Pixel, die keine Abhängigkeiten zum DOM haben. Die Ausführungsreihenfolge ist nicht garantiert.
defer — Parallel laden, nach dem Parsing ausführen
Mit defer wird das Script parallel geladen, aber erst ausgeführt, nachdem das gesamte HTML geparst wurde. Die Ausführungsreihenfolge bleibt dabei erhalten.
<!-- Script wird parallel geladen und erst nach dem HTML-Parsing ausgeführt -->
<script defer src="main.js"></script>
<script defer src="components.js"></script>
<!-- Reihenfolge: main.js vor components.js -->
Wann verwenden? Für alle Scripts, die auf DOM-Elemente zugreifen oder Abhängigkeiten haben. In den meisten Fällen ist defer die bessere Wahl gegenüber async.
| Eigenschaft | Normal | async | defer |
|---|---|---|---|
| HTML-Parsing blockiert? | Ja | Beim Ausführen | Nein |
| Download parallel? | Nein | Ja | Ja |
| Reihenfolge garantiert? | Ja | Nein | Ja |
| Ausführungszeitpunkt | Sofort | Nach Download | Nach HTML-Parsing |
Lazy Loading: Nur laden, was sichtbar ist
Lazy Loading ist eine Technik, bei der Ressourcen erst dann geladen werden, wenn sie tatsächlich im Viewport des Nutzers sichtbar sind. Das reduziert die initiale Ladezeit erheblich — besonders auf langen Seiten mit vielen Bildern oder Komponenten.
Lazy Loading für Bilder (nativ im Browser)
Seit 2020 unterstützen alle modernen Browser das native loading="lazy"-Attribut für Bilder. Das ist die einfachste und performanteste Methode:
<!-- Ohne Lazy Loading: Bild wird sofort geladen -->
<img src="grosses-foto.jpg" alt="Beispiel">
<!-- Mit Lazy Loading: Bild wird erst geladen wenn es in den Viewport kommt -->
<img src="grosses-foto.jpg" alt="Beispiel" loading="lazy" width="800" height="600">
Wichtig für SEO: Gib immer width und height an. Ohne diese Attribute reserviert der Browser keinen Platz für das Bild — sobald es lädt, verschiebt sich der Layout (CLS steigt). Das schadet dem Ranking. Mehr zu Bildoptimierung generell findest du im nächsten Artikel dieser Reihe.
Lazy Loading für Iframes
Das gleiche loading="lazy"-Attribut funktioniert auch für <iframe>-Elemente — ideal für eingebettete YouTube-Videos, Google Maps oder andere externe Inhalte:
<!-- YouTube-Video nur laden wenn sichtbar -->
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
loading="lazy"
width="560"
height="315"
title="Beschreibender Titel für SEO">
</iframe>
Lazy Loading für JavaScript-Komponenten (Intersection Observer)
Für komplexere Anwendungsfälle — zum Beispiel wenn du eine Karte, einen Chat-Widget oder einen aufwändigen Slider erst laden willst wenn er sichtbar ist — nutze den IntersectionObserver:
// Beobachte ein Element und lade Code erst wenn es sichtbar wird
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element ist im Viewport — jetzt laden
import('./heavy-component.js').then(module => {
module.init(entry.target);
});
observer.unobserve(entry.target); // Nur einmal laden
}
});
}, { threshold: 0.1 }); // Bereits bei 10% Sichtbarkeit laden
// Element beobachten
const widget = document.getElementById('heavy-widget');
if (widget) observer.observe(widget);
Code Splitting: Nur laden was gebraucht wird
Moderne JavaScript-Frameworks (React, Vue, Angular) bundeln oft den gesamten Anwendungscode in eine große Datei. Ein Bundle von 500 KB+ ist keine Seltenheit. Das Problem: Der Browser muss diese gesamte Datei herunterladen, parsen und ausführen — auch wenn der Nutzer nur die Startseite besucht.
Code Splitting teilt dieses Bundle in kleinere Teile auf, die bei Bedarf geladen werden.
Dynamische Imports (JavaScript-Standard)
// Ohne Code Splitting — alles wird sofort geladen
import { HeavyChart } from './charts';
import { AdvancedMap } from './map';
// Mit dynamischen Imports — nur laden wenn benötigt
async function showChart() {
const { HeavyChart } = await import('./charts');
new HeavyChart('#container');
}
// Auf Nutzerinteraktion reagieren
document.getElementById('show-chart-btn').addEventListener('click', showChart);
Code Splitting in React (React.lazy)
import React, { lazy, Suspense } from 'react';
// Komponente wird erst geladen wenn sie gerendert wird
const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
function App() {
return (
<Suspense fallback={<div>Lädt...</div>}>
<HeavyDashboard />
</Suspense>
);
}
Route-based Code Splitting (empfohlen)
Die effektivste Methode ist Route-based Code Splitting: Jede Seite lädt nur den Code, den sie braucht. Bei einem Webshop muss die Startseite nicht den Code für die Checkout-Seite laden:
// React Router mit Code Splitting
import { lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
const HomePage = lazy(() => import('./pages/Home'));
const ProductPage = lazy(() => import('./pages/Product'));
const CheckoutPage = lazy(() => import('./pages/Checkout'));
// Jede Route lädt nur ihr eigenes Bundle
Tree Shaking: Toten Code entfernen
Tree Shaking entfernt JavaScript-Code, der zwar importiert aber nie verwendet wird. Wenn du eine große Utility-Bibliothek importierst aber nur eine Funktion nutzt, sollte nur diese eine Funktion im finalen Bundle landen:
// Schlecht: Importiert die gesamte Lodash-Bibliothek (~70 KB)
import _ from 'lodash';
const result = _.get(obj, 'path.to.value');
// Gut: Importiert nur die eine benötigte Funktion (~1 KB)
import get from 'lodash/get';
const result = get(obj, 'path.to.value');
// Noch besser: Nutze ES6-Module für automatisches Tree Shaking
import { debounce } from 'lodash-es'; // Bundler entfernt den Rest
Bundler wie Webpack, Rollup oder Vite unterstützen Tree Shaking automatisch — aber nur bei ES-Module-Syntax (import/export), nicht bei CommonJS (require).
Preload, Prefetch und Preconnect: Ressourcen vorausschauend laden
Manchmal weißt du schon beim Laden der Seite, welche Ressourcen gleich benötigt werden. Mit Resource Hints kannst du den Browser darauf hinweisen:
<!-- Kritisches Script sofort laden (höchste Priorität) -->
<link rel="preload" href="critical.js" as="script">
<!-- Nächste Seite vorbereiten (Nutzer wird wahrscheinlich navigieren) -->
<link rel="prefetch" href="next-page.js">
<!-- DNS-Lookup für externe Domain schon vorbereiten -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
JavaScript und Core Web Vitals
Schlechtes JavaScript-Management ist einer der Hauptgründe für schlechte Core Web Vitals. Konkret:
- LCP (Largest Contentful Paint): Render-Blocking JS verzögert den LCP. Ziel: unter 2,5 Sekunden
- CLS (Cumulative Layout Shift): JS das nachträglich Elemente einfügt ohne reservierten Platz → Layout springt
- INP (Interaction to Next Paint): Langer Main Thread durch schweres JS → Klicks reagieren langsam. Ziel: unter 200 ms
Für INP ist besonders wichtig: Teile lange JavaScript-Aufgaben in kleinere Chunks auf. Aufgaben über 50 ms blockieren den Main Thread und machen die Seite "träge":
// Schlechtes Beispiel: Eine riesige Aufgabe blockiert den Main Thread
function processLargeArray(arr) {
arr.forEach(item => heavyCalculation(item));
}
// Besseres Beispiel: Aufgabe in Chunks aufteilen
async function processLargeArrayChunked(arr) {
const CHUNK_SIZE = 50;
for (let i = 0; i < arr.length; i += CHUNK_SIZE) {
const chunk = arr.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => heavyCalculation(item));
// Dem Browser kurz Zeit zum Atmen geben (für INP)
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Mit unserem JavaScript-SEO-Checker kannst du prüfen, ob deine Website JavaScript-abhängige Inhalte hat, die Google möglicherweise nicht sofort indexiert.
Praktische JavaScript-Performance-Checkliste für SEO
Scripts mit defer oder async laden
Alle nicht-kritischen Scripts aus dem <head> entfernen oder mit defer/async versehen
Bilder mit loading="lazy" versehen
Alle Bilder außerhalb des initialen Viewports — mit width/height-Attributen
JavaScript-Bundle-Größe prüfen
Ziel: unter 150 KB (gzipped) für das initiale Bundle. Größere Bundles aufteilen.
Kritische Inhalte im HTML-Quellcode
H1, Meta-Tags, Haupttexte müssen im HTML stehen — nicht erst durch JS eingefügt werden
Lange Tasks aufteilen (für INP)
Aufgaben über 50 ms in Chunks aufteilen — Ziel: INP unter 200 ms
JavaScript minifizieren und komprimieren
Whitespace, Kommentare und lange Variablennamen entfernen. Gzip/Brotli auf dem Server aktivieren.
Tools zur JavaScript-Performance-Analyse
Folgende kostenlose Tools helfen dir bei der Analyse:
- Chrome DevTools → Coverage-Tab: Zeigt welcher JavaScript-Code beim Seitenaufruf ungenutzt bleibt
- Chrome DevTools → Performance-Tab: Visualisiert den Main Thread und zeigt lange Tasks
- Lighthouse (in Chrome DevTools): Gibt konkrete JavaScript-Empfehlungen mit Einsparpotenzial in Millisekunden
- Unser Page-Speed-Estimator: Schnelle Einschätzung der Ladezeit deiner Website
- Unser JavaScript-SEO-Checker: Prüft ob Google deine JS-abhängigen Inhalte indexieren kann
Fazit: JavaScript ist Fluch und Segen für SEO
JavaScript ist unvermeidlich — aber schlecht geladenes JavaScript kostet dich Rankings. Die gute Nachricht: Die wichtigsten Optimierungen sind einfach umzusetzen.
Starte mit den drei wichtigsten Maßnahmen:
- Alle Scripts auf defer umstellen — das ist die schnellste und einfachste Verbesserung
- loading="lazy" auf alle Bilder unten auf der Seite — mit width/height-Attributen
- Kritische Inhalte ins HTML verlagern — H1, Meta-Description und Haupttext müssen im Quellcode stehen
Diese drei Schritte allein können deinen Page Speed Score erheblich verbessern und helfen Google, deine Seiten schneller zu indexieren. Im nächsten Artikel des Performance-Clusters schauen wir uns die Bildoptimierung genauer an — WebP, AVIF und fortgeschrittenes Lazy Loading für noch bessere Ladezeiten.