Warum JavaScript das größte Performance-Problem des modernen Webs ist
Mal ehrlich: JavaScript hat das Web revolutioniert – und gleichzeitig zu seinem gravierendsten Performance-Engpass gemacht. Laut dem HTTP Archive Web Almanac 2025 lädt die durchschnittliche Desktop-Startseite mittlerweile 697 KB JavaScript, auf Mobilgeräten sind es 632 KB. Im Vergleich zum Vorjahr (620 KB Desktop, 570 KB Mobil) ist das ein deutlicher Anstieg. JavaScript wächst schneller als jede andere Ressourcenkategorie im Web – und das sollte uns zu denken geben.
Noch alarmierender: JavaScript hat Bilder als ressourcenreichste Dateikategorie nach Anzahl der Requests überholt. Die mediane Desktop-Seite lädt inzwischen 24 JavaScript-Dateien gegenüber 18 Bildern. In absoluten Bytes dominieren Bilder zwar noch (1.054 KB vs. 613 KB), aber der Trend ist eindeutig – und das eigentliche Problem liegt ohnehin nicht in der reinen Dateigröße.
JavaScript unterliegt nämlich einer sogenannten „Doppelbesteuerung": Jedes Byte muss erst heruntergeladen und dann vom Browser geparst, kompiliert und ausgeführt werden. Ein 500 KB großes Bild? Nach dem Download sofort sichtbar. 500 KB JavaScript? Das blockiert potenziell den Haupt-Thread für mehrere hundert Millisekunden – und genau das spüren Ihre Nutzer als Trägheit und Unresponsivität. Das HTTP Archive bringt es treffend auf den Punkt: JavaScript trägt „eine Performance-Steuer, die weit schwerer wiegt als seine Dateigröße vermuten lässt".
In unserem Leitfaden zu den Core Web Vitals 2026 haben wir bereits gezeigt, wie INP (Interaction to Next Paint) seit März 2024 als neue Responsiveness-Metrik die deutlich schwächere FID-Metrik ersetzt hat. INP misst die gesamte Zeitspanne jeder Nutzerinteraktion – und zu viel JavaScript ist der häufigste Grund für schlechte INP-Werte. Ebenso beeinflusst JavaScript den Largest Contentful Paint (LCP), wenn render-blockierende Skripte das Laden kritischer Inhalte verzögern.
Dieser Artikel ist Ihr Praxis-Leitfaden zur JavaScript-Performance-Optimierung. Wir gehen gemeinsam durch Bundle-Analyse, Tree Shaking, Code-Splitting, Third-Party-Script-Management und moderne Browser-APIs – mit konkreten Codebeispielen, die Sie direkt in Ihren Projekten einsetzen können. Also, legen wir los.
Bundle-Analyse: Bevor Sie optimieren, müssen Sie messen
Die erste Regel der Performance-Optimierung lautet: Messen, nicht raten. Ich kann das gar nicht genug betonen. Bevor Sie eine einzige Zeile Code ändern, müssen Sie verstehen, woraus Ihr JavaScript-Bundle eigentlich besteht, welche Module wie viel Platz beanspruchen und wo die größten Einsparpotenziale liegen.
Ohne diese Diagnose optimieren Sie im Blindflug – und riskieren, Zeit in die falschen Stellschrauben zu investieren.
Webpack Bundle Analyzer
Der Webpack Bundle Analyzer ist nach wie vor das Standardwerkzeug zur visuellen Bundle-Analyse. Er erzeugt eine interaktive Treemap, die jedes Modul proportional zu seiner Größe darstellt – ein Blick genügt, um die größten Brocken zu identifizieren.
# Installation
npm install --save-dev webpack-bundle-analyzer
# In webpack.config.js
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
defaultSizes: 'gzip'
})
]
};
Was findet man typischerweise bei einer Bundle-Analyse? Hier die üblichen Verdächtigen:
- Überdimensionierte Bibliotheken: Moment.js (330 KB) statt day.js (2 KB), lodash komplett (72 KB) statt lodash-es mit Tree Shaking (4–8 KB), die vollständige uuid-Bibliothek statt des nativen
crypto.randomUUID() - Doppelte Dependencies: Zwei verschiedene Versionen derselben Bibliothek im Bundle – ein häufiges Problem in größeren Projekten mit verschachtelten Abhängigkeiten
- Ungenutzter Code: Ganze Module, die importiert aber nie verwendet werden, oder Feature-Flags für längst abgeschlossene A/B-Tests (wir hatten alle schon solche Leichen im Keller)
- Fehlende Code-Splitting-Möglichkeiten: Routen-spezifischer Code im Haupt-Bundle, obwohl er nur auf einer einzigen Unterseite benötigt wird
Vite und Rollup: rollup-plugin-visualizer
Wenn Sie Vite als Build-Tool verwenden (was 2026 zunehmend zum Standard wird), nutzen Sie stattdessen den rollup-plugin-visualizer:
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/bundle-stats.html',
gzipSize: true,
brotliSize: true,
template: 'treemap'
})
]
});
Chrome DevTools: Coverage-Tab
Ein oft übersehenes Werkzeug – und ehrlich gesagt eines meiner liebsten – ist der Coverage-Tab in den Chrome DevTools (erreichbar über Ctrl+Shift+P → „Show Coverage"). Er zeigt Ihnen in Echtzeit, wie viel Prozent des heruntergeladenen JavaScript-Codes tatsächlich beim Seitenaufruf ausgeführt wird. Bei vielen Websites liegt der Anteil ungenutzten Codes bei 50 bis 70 Prozent. Das ist ein enormes Optimierungspotenzial, das direkt vor Ihnen liegt.
Öffnen Sie den Coverage-Tab, laden Sie Ihre Seite neu, und sortieren Sie nach „Unused Bytes". Die Dateien mit dem höchsten Anteil ungenutzten Codes sind Ihre primären Kandidaten für Code-Splitting. Laut dem Web Almanac 2024 könnten bei der medianen Seite rund 12 KB JavaScript allein durch Minifizierung eingespart werden – und 82,7 Prozent der verschwendeten Bytes stammen von First-Party-Code, nicht von Drittanbietern. Der Ball liegt also bei uns selbst.
Tree Shaking: Toten Code automatisch eliminieren
Tree Shaking bezeichnet die automatische Entfernung von ungenutztem Code durch den Bundler. Der Name kommt von der Metapher, einen Baum zu schütteln, sodass die toten Blätter herunterfallen – ein schönes Bild, finde ich. Damit Tree Shaking funktioniert, müssen zwei Voraussetzungen erfüllt sein: ES-Module-Syntax und eine korrekte Side-Effects-Konfiguration.
ES Modules statt CommonJS
Tree Shaking funktioniert nur mit ES-Module-Imports (import/export), nicht mit CommonJS (require/module.exports). Der Grund ist simpel: ES-Module-Imports sind statisch analysierbar – der Bundler kann zur Build-Zeit exakt ermitteln, welche Exporte tatsächlich verwendet werden. CommonJS-Imports hingegen sind dynamisch und können zur Laufzeit berechnet werden, was eine statische Analyse unmöglich macht.
// SCHLECHT: CommonJS – kein Tree Shaking möglich
const _ = require('lodash');
const result = _.groupBy(data, 'category');
// → Gesamtes lodash (72 KB) im Bundle
// GUT: ES-Module-Import – Tree Shaking funktioniert
import { groupBy } from 'lodash-es';
const result = groupBy(data, 'category');
// → Nur groupBy und seine Abhängigkeiten (~4 KB)
// NOCH BESSER: Direkt-Import des einzelnen Moduls
import groupBy from 'lodash-es/groupBy';
const result = groupBy(data, 'category');
// → Minimaler Footprint
Der Unterschied zwischen 72 KB und 4 KB – nur durch die Import-Methode. Das muss man sich mal auf der Zunge zergehen lassen.
Side Effects richtig konfigurieren
Selbst mit ES Modules kann der Bundler Code nicht entfernen, wenn er vermutet, dass der Import Seiteneffekte hat – also Code, der beim bloßen Importieren Dinge verändert (globale Variablen, CSS-Injektionen, Polyfills). Die sideEffects-Angabe in der package.json teilt dem Bundler mit, welche Dateien sicher entfernt werden können:
// package.json – Keine Datei hat Seiteneffekte
{
"name": "mein-projekt",
"sideEffects": false
}
// Oder selektiv: Nur CSS und Polyfills haben Seiteneffekte
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
In Webpack aktivieren Sie Tree Shaking im Production-Modus automatisch. Die relevanten Einstellungen sind usedExports: true (markiert ungenutzten Code) und minimize: true (entfernt den markierten Code). Zusätzlich sorgt concatenateModules: true (Scope Hoisting) für kleinere Bundles, indem Module zusammengefasst werden:
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true
}
};
Typische Tree-Shaking-Fallen
In der Praxis scheitert Tree Shaking leider häufiger als man denkt. Hier die typischen Stolperfallen:
- Barrel-Exports: Eine
index.js, die alles re-exportiert (export * from './module'), kann Tree Shaking erschweren, weil der Bundler alle transitiven Abhängigkeiten auflösen muss. Bei Rollup kann diepreserveModules-Einstellung helfen, die Modulstruktur beizubehalten und damit das Tree Shaking zu verbessern. - Transpiler-Probleme: Wenn Babel oder TypeScript ES Modules zu CommonJS transpilieren, geht die statische Analysierbarkeit verloren. Stellen Sie sicher, dass
modules: falsein Ihrer Babel-Konfiguration gesetzt ist und TypeScript mit"module": "esnext"konfiguriert ist. - UI-Bibliotheken: Viele Component-Libraries wie ältere Versionen von Ant Design oder Material-UI erfordern spezielle Import-Strategien für effektives Tree Shaking. Hier hilft nur ein Blick in die jeweilige Dokumentation.
- Re-Exports mit Seiteneffekten: Wenn ein re-exportiertes Modul selbst Seiteneffekte hat (z.B. einen globalen Event-Listener registriert), kann es nicht durch Tree Shaking entfernt werden – auch wenn der eigentliche Export ungenutzt ist. Das ist besonders tückisch, weil es nicht auf den ersten Blick erkennbar ist.
Code-Splitting: Die richtige Menge Code zur richtigen Zeit
Code-Splitting ist die Strategie, Ihr JavaScript-Bundle in mehrere kleinere Chunks aufzuteilen, die on demand geladen werden. Statt einem monolithischen Bundle von 500 KB, das komplett heruntergeladen werden muss, bevor irgendetwas passiert, laden Sie zunächst nur die 80 KB, die für die aktuelle Seite nötig sind – und den Rest erst, wenn er tatsächlich gebraucht wird.
Die Ergebnisse sprechen für sich: Implementiertes Code-Splitting kann die initiale Ladezeit um bis zu 60 Prozent reduzieren. Anwendungen mit routenbasiertem Code-Splitting verzeichnen eine durchschnittliche Reduktion der Ladezeit um 30 Prozent gegenüber nicht-gesplitteten Anwendungen. Das wirkt sich direkt auf LCP und Time to Interactive aus.
Route-basiertes Code-Splitting
Die effektivste (und risikoärmste) Form des Code-Splittings ist die Aufteilung nach Routen. Jede Seite Ihrer Anwendung wird zu einem eigenen Chunk, der nur geladen wird, wenn der Nutzer diese Seite tatsächlich besucht.
React mit React Router und React.lazy:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Dynamische Imports erzeugen separate Chunks:
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Laden...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Jeder import()-Aufruf erzeugt einen separaten Chunk. Der Browser lädt den Dashboard-Chunk beim ersten Besuch, den Settings-Chunk erst, wenn der Nutzer die Einstellungsseite aufruft, und so weiter. In Next.js geschieht dieses route-basierte Code-Splitting übrigens automatisch: Jede Datei im pages/- oder app/-Verzeichnis wird zu einem separaten Chunk – kein manuelles Eingreifen nötig. Ziemlich elegant.
Component-Level Code-Splitting
Neben ganzen Seiten können Sie auch einzelne, schwere Komponenten lazy-loaden. Das ist besonders sinnvoll für Elemente, die nicht sofort sichtbar sind oder erst nach einer Nutzerinteraktion erscheinen – Modale, Chart-Bibliotheken oder Rich-Text-Editoren zum Beispiel:
import { lazy, Suspense, useState } from 'react';
// Schwere Komponente: Chart-Bibliothek (~180 KB)
const AnalyticsChart = lazy(
() => import('./components/AnalyticsChart')
);
// Modal mit reichem Editor (~120 KB)
const RichTextEditor = lazy(
() => import('./components/RichTextEditor')
);
function ProductPage() {
const [showChart, setShowChart] = useState(false);
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<h1>Produktdetails</h1>
<button onClick={() => setShowChart(true)}>
Statistiken anzeigen
</button>
{showChart && (
<Suspense fallback={<p>Chart lädt...</p>}>
<AnalyticsChart productId={123} />
</Suspense>
)}
<button onClick={() => setShowEditor(true)}>
Beschreibung bearbeiten
</button>
{showEditor && (
<Suspense fallback={<p>Editor lädt...</p>}>
<RichTextEditor />
</Suspense>
)}
</div>
);
}
Durch diesen Ansatz werden die 300 KB für Chart und Editor erst heruntergeladen, wenn der Nutzer sie tatsächlich braucht. Für Nutzer, die nur die Produktdetails lesen wollen, spart das erheblich Bandbreite und Parsing-Zeit.
Vendor-Splitting für optimales Caching
Ein weiterer wichtiger Aspekt (den viele Teams übersehen) ist die Trennung von Vendor-Code und Anwendungscode. Die Idee dahinter: Ihre eigenen Quelldateien ändern sich bei jedem Deployment, aber Bibliotheken wie React, Vue oder Lodash bleiben oft monatelang auf derselben Version. Durch die Aufteilung in separate Chunks kann der Browser den Vendor-Chunk langfristig cachen:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
chunks: 'all',
priority: 20
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
In Vite konfigurieren Sie die Chunk-Aufteilung über die Rollup-Optionen:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
'router': ['react-router-dom'],
'ui-lib': [
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu'
]
}
}
}
}
});
Third-Party-Scripts: Die versteckte Performance-Zeitbombe
Jetzt wird es spannend – und ein bisschen frustrierend. Die vielleicht unterschätzteste Quelle von JavaScript-Performance-Problemen sind Third-Party-Scripts: Analytics-Tools, Werbeplattformen, Chat-Widgets, Social-Media-Integrationen. Laut dem Web Almanac 2025 verwenden über 90 Prozent aller Webseiten mindestens ein Third-Party-Script.
Das Problem? Diese Skripte können Ladezeiten um 500 bis 1.500 Millisekunden verzögern und den Haupt-Thread für bis zu 1.640 Millisekunden blockieren. Besonders problematisch sind Tag Manager, die ihrerseits viele weitere Skripte nachladen, und Customer-Success-Scripts (Chat-Lösungen), die generell überdurchschnittlich viel wiegen. Anders als bei Ihrem eigenen Code haben Sie über deren Qualität und Größe nur begrenzte Kontrolle – und das macht die Sache so ärgerlich.
Das Web Almanac 2025 zeigt auch den direkten Zusammenhang mit Core Web Vitals: Die beliebtesten Websites mit komplexen Third-Party-Integrationen haben trotz eines Anstiegs guter INP-Werte von 53 auf 63 Prozent bei den Top-1.000-Websites immer noch die größten Schwierigkeiten mit Responsiveness. Mehr JavaScript, mehr Funktionalität, mehr Drittanbieter – mehr Probleme.
async vs. defer: Das Minimum an Kontrolle
Nicht-kritische Drittanbieter-Skripte sollten niemals render-blockierend geladen werden. Die Attribute async und defer verhindern das – aber sie unterscheiden sich fundamental:
<!-- SCHLECHT: Render-blockierend -->
<script src="https://example.com/analytics.js"></script>
<!-- async: Download parallel, Ausführung sofort nach Download
Gut für: unabhängige Skripte wie Analytics -->
<script async src="https://example.com/analytics.js">
</script>
<!-- defer: Download parallel, Ausführung nach HTML-Parsing
Gut für: Skripte, die auf das DOM zugreifen -->
<script defer src="https://example.com/widget.js">
</script>
Die Faustregel: Verwenden Sie defer als Standard für die meisten Drittanbieter-Skripte. async eignet sich für Skripte, die wirklich unabhängig vom DOM und voneinander sind, wie reine Analytics-Snippets. Und vergessen Sie nicht: Auch async-Skripte können den Haupt-Thread blockieren – sie werden nur nicht-render-blockierend heruntergeladen.
Das Facade-Pattern: Import on Interaction
Schwere Einbettungen wie YouTube-Videos, Chat-Widgets oder Social-Media-Feeds müssen nicht sofort geladen werden. Das Facade-Pattern (auch „Import on Interaction" genannt) ersetzt sie durch einen leichtgewichtigen Platzhalter, der erst bei Nutzerinteraktion das echte Widget nachlädt. In meiner Erfahrung ist das einer der Hebel mit dem besten Aufwand-Nutzen-Verhältnis:
function YouTubeFacade({ videoId, title }) {
const [isLoaded, setIsLoaded] = useState(false);
if (isLoaded) {
return (
<iframe
width="560"
height="315"
src={`https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1`}
title={title}
allow="autoplay; encrypted-media"
allowFullScreen
/>
);
}
return (
<button
onClick={() => setIsLoaded(true)}
className="youtube-facade"
style={{
backgroundImage:
`url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`
}}
aria-label={`Video abspielen: ${title}`}
>
<svg viewBox="0 0 68 48" width="68" height="48">
<path d="M66.5 7.7c-.8-2.9-2.5-5.4-5.4-6.2
C55.8.1 34 0 34 0S12.2.1 6.9 1.6c-3 .7-4.6
3.3-5.4 6.1C.1 13 0 24 0 24s.1 11 1.5 16.3
c.8 2.9 2.5 5.4 5.4 6.2C12.2 47.9 34 48 34
48s21.8-.1 27.1-1.6c3-.7 4.6-3.3 5.4-6.1
C67.9 35 68 24 68 24s-.1-11-1.5-16.3z"
fill="red"/>
<path d="M45 24L27 14v20" fill="white"/>
</svg>
</button>
);
}
Durch dieses Muster sparen Sie typischerweise 500 KB bis 1 MB an JavaScript pro YouTube-Einbettung – und das wird nur geladen, wenn der Nutzer tatsächlich auf Play klickt. Bibliotheken wie lite-youtube-embed bieten fertige, produktionsreife Implementierungen dieses Patterns.
Web Worker für Third-Party-Scripts: Partytown
Ein ziemlich cleverer Ansatz ist die Auslagerung von Drittanbieter-Skripten in einen Web Worker, um den Haupt-Thread vollständig zu entlasten. Die Bibliothek Partytown macht genau das – und die Einrichtung ist überraschend unkompliziert:
<script>
partytown = {
forward: ['dataLayer.push', 'gtag'],
debug: false
};
</script>
<script src="/~partytown/partytown.js"></script>
<!-- type="text/partytown" statt type="text/javascript" -->
<script type="text/partytown"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX">
</script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
</script>
Partytown verlagert die Ausführung von Analytics, Tag Manager und Werbe-Skripten vollständig in einen separaten Thread. Der Haupt-Thread bleibt frei für Rendering und Nutzerinteraktionen – mit messbaren Verbesserungen bei INP und Total Blocking Time.
Ein Wort der Warnung allerdings: Partytown ist nicht für alle Skripte geeignet. Scripts, die direkt und synchron auf das DOM zugreifen, können Probleme verursachen. Testen Sie gründlich.
Moderne Browser-APIs für JavaScript-Loading
2026 stehen uns mehrere leistungsstarke Browser-APIs zur Verfügung, die das Laden und Ausführen von JavaScript grundlegend verändern. Drei davon verdienen besondere Aufmerksamkeit.
Import Maps: Module ohne Bundler auflösen
Import Maps sind seit 2025 in allen großen Browsern verfügbar und erlauben es, sogenannte „Bare Specifiers" direkt im Browser aufzulösen – ganz ohne Bundler. Das war vorher ein exklusives Feature von Build-Tools. Import Maps sind besonders für kleinere Projekte, Prototypen und progressive Enhancement interessant:
<script type="importmap">
{
"imports": {
"preact": "https://esm.sh/[email protected]",
"preact/hooks": "https://esm.sh/[email protected]/hooks",
"htm": "https://esm.sh/[email protected]"
}
}
</script>
<script type="module">
import { h, render } from 'preact';
import { useState } from 'preact/hooks';
import htm from 'htm';
const html = htm.bind(h);
function Counter() {
const [count, setCount] = useState(0);
return html`<button onClick=${() => setCount(count + 1)}>
Klicks: ${count}
</button>`;
}
render(html`<${Counter} />`, document.getElementById('app'));
</script>
Import Maps unterstützen auch Scopes (unterschiedliche Modul-Versionen pro Pfad) und Integrity-Hashes zur Sicherheitsvalidierung. Für Produktions-Anwendungen mit Build-Pipeline bleiben Bundler weiterhin die bessere Wahl, aber für bestimmte Anwendungsfälle bieten Import Maps eine erfrischend einfache Zero-Build-Lösung.
modulepreload: Module vorab laden und kompilieren
Das <link rel="modulepreload">-Attribut geht über einfaches Preloading hinaus: Es weist den Browser an, ein JavaScript-Modul nicht nur herunterzuladen, sondern auch sofort zu parsen und kompilieren. Wenn das Modul dann tatsächlich ausgeführt wird, ist es bereits bereit – die sonst anfallende Parse- und Kompilierungszeit entfällt komplett.
<head>
<!-- Kritische Module vorab laden UND kompilieren -->
<link rel="modulepreload" href="/js/app.js">
<link rel="modulepreload" href="/js/router.js">
<link rel="modulepreload" href="/js/store.js">
</head>
<!-- Modul ist beim Ausführen bereits kompiliert -->
<script type="module" src="/js/app.js"></script>
Der entscheidende Vorteil gegenüber rel="preload": Bei modulepreload entfällt die Parse- und Kompilierungszeit zum Ausführungszeitpunkt vollständig. Bei großen Modulen kann das 100 bis 300 Millisekunden einsparen – das ist spürbar. Außerdem kann der Browser die Abhängigkeiten des Moduls automatisch ebenfalls vorladen, was Sie bei regulärem preload manuell für jede einzelne Abhängigkeit tun müssten.
Speculation Rules API: Die nächste Seite vorab rendern
Die Speculation Rules API ist aus meiner Sicht eine der aufregendsten Browser-Neuerungen der letzten Jahre. Sie ermöglicht es, komplette Seiten vorab zu laden (Prefetch) oder sogar komplett vorzurendern (Prerender) – bevor der Nutzer darauf klickt. Das ist kein Prefetch einzelner Ressourcen, sondern das Vorbereiten einer ganzen Navigation.
Die Wirkung ist beeindruckend: Vorab gerenderte Seiten erscheinen praktisch sofort – als würde der Nutzer zwischen bereits geöffneten Tabs wechseln.
<script type="speculationrules">
{
"prerender": [
{
"source": "document",
"where": {
"and": [
{ "href_matches": "/produkte/*" },
{ "not": { "selector_matches": ".no-prerender" } }
]
},
"eagerness": "moderate"
}
],
"prefetch": [
{
"source": "document",
"where": { "href_matches": "/blog/*" },
"eagerness": "conservative"
}
]
}
</script>
Die eagerness-Stufen steuern, wie aggressiv der Browser spekuliert:
- immediate: Sofort nach dem Erkennen der Regel spekulieren
- eager: So früh wie möglich, aber mit Browser-Ermessen
- moderate: Beim Hover über den Link – ein guter Kompromiss zwischen Geschwindigkeit und Bandbreitenverbrauch
- conservative: Erst beim Mousedown oder Touchstart – minimaler Bandbreitenverbrauch, aber auch die kürzeste Vorbereitungszeit
Wichtig: Die Speculation Rules API wird derzeit vollständig nur von Chromium-Browsern (Chrome, Edge) unterstützt. Andere Browser ignorieren die Regeln einfach – es gibt also keinen Nachteil. Sie können die API bedenkenlos als Progressive Enhancement einsetzen.
Bundler-Wahl 2026: Vite, Webpack und die Rolldown-Revolution
Die Wahl des richtigen Bundlers hat enormen Einfluss auf Ihre Build-Performance und die Qualität der erzeugten Bundles. 2026 zeichnet sich ein ziemlich klares Bild ab.
Vite: Der neue Standard
Vite hat sich als De-facto-Standard für neue Webprojekte etabliert – und das verdient. Im Entwicklungsmodus nutzt es native ES Modules und esbuild für blitzschnelle Hot Module Replacement (HMR), für Production-Builds setzt es auf Rollup. Die Ergebnisse: hervorragendes Tree Shaking und ein durchschnittliches Bundle, das etwa 13 Prozent kleiner ausfällt als Webpack-Äquivalente. Die Konfiguration ist minimal, das Ökosystem wächst rasant, und die Developer Experience ist schlicht exzellent.
Rolldown + Oxc: Der Game-Changer
Die spannendste Entwicklung in der Build-Tool-Landschaft ist ohne Frage Rolldown – ein in Rust geschriebener Bundler, der als Rollup-kompatibler Ersatz konzipiert ist und bereits als experimentelles Backend in Vite integriert wird.
Die Benchmark-Ergebnisse sind, nun ja, beeindruckend wäre fast untertrieben: GitLab hat mit rolldown-vite 2,6-mal schnellere Builds als mit Standard-Vite erzielt. Mit allen nativen Plugins aktiviert, reduzierte sich die Build-Zeit von 2,5 Minuten auf nur 22 Sekunden – das ist 43-mal schneller als ihr existierender Webpack-Build.
In Kombination mit dem Oxc-Minifier (ebenfalls in Rust) erzeugt Rolldown Bundles, die nochmals 18 Prozent kleiner sind als die Rollup-Äquivalente. Der Schlüssel: mehrfache Dead-Code-Elimination-Passes, ähnlich wie bei Rollup, aber mit minimalem Performance-Overhead dank der nativen Rust-Implementierung.
Webpack: Immer noch relevant
Webpack bleibt für bestehende Projekte und komplexe Enterprise-Anwendungen relevant. Die Konfigurierbarkeit ist unübertroffen, das Ökosystem an Plugins riesig, und für Projekte mit spezifischen Anforderungen (Module Federation, Custom Loader-Chains) gibt es oft keine Alternative. Für neue Projekte würde ich 2026 allerdings den Einstieg mit Vite empfehlen – die Developer Experience und die Bundle-Qualität sprechen einfach klar dafür.
Performance Budgets: Grenzen setzen und automatisch durchsetzen
Alle bisherigen Optimierungen bringen langfristig wenig, wenn neue Features und Bibliotheken die JavaScript-Größe unkontrolliert wieder aufblähen. Ich habe das schon bei zu vielen Projekten erlebt: Nach der großen Optimierungsrunde schleicht sich innerhalb weniger Monate wieder alles ein. Performance Budgets sind die Lösung – feste Obergrenzen, die in der CI/CD-Pipeline automatisch überprüft werden und den Build fehlschlagen lassen, wenn sie überschritten werden.
Budgets in der Build-Konfiguration
Webpack bietet eingebaute Performance-Budgets, die sich direkt in der Konfiguration definieren lassen:
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 300000,
hints: 'error'
}
};
Lighthouse CI in der Pipeline
Für eine ganzheitliche Performance-Überwachung integrieren Sie Lighthouse CI in Ihre CI/CD-Pipeline. So erkennen Sie Performance-Regressionen in jedem Pull Request – nicht erst nach dem Deployment:
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: pull_request
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci && npm run build
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
configPath: .lighthouserc.json
uploadArtifacts: true
// .lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"total-byte-weight": [
"error", { "maxNumericValue": 500000 }
],
"mainthread-work-breakdown": [
"warn", { "maxNumericValue": 4000 }
],
"interactive": [
"error", { "maxNumericValue": 3500 }
]
}
}
}
}
Zusätzlich bieten Tools wie Codecov Bundle Analysis die Möglichkeit, Bundle-Größenänderungen direkt in Pull Requests anzuzeigen – so sehen Entwickler sofort, wenn ihre Änderungen das Budget sprengen würden.
Praxis-Checkliste: JavaScript-Performance in 10 Schritten
Zum Abschluss eine priorisierte Checkliste, die Sie als Ausgangspunkt für Ihre Optimierung nutzen können. Arbeiten Sie sie am besten von oben nach unten ab – die Punkte sind nach Wirksamkeit und Aufwand sortiert:
- Bundle analysieren: Webpack Bundle Analyzer oder rollup-plugin-visualizer einrichten und die größten Module identifizieren
- Schwergewichte ersetzen: Moment.js durch day.js, lodash durch lodash-es mit gezielten Imports, uuid durch
crypto.randomUUID() - Tree Shaking verifizieren: ES-Module-Imports verwenden,
sideEffectsin package.json konfigurieren, Babel aufmodules: falsesetzen - Route-basiertes Code-Splitting: Jede Route als separaten Chunk laden mit
React.lazy/Suspenseoder dynamischen Imports - Vendor-Splitting konfigurieren: Frameworks und große Libraries in eigene, langfristig cachbare Chunks auslagern
- Third-Party-Scripts auditieren: Alle externen Skripte inventarisieren, async/defer nutzen, Facades für schwere Einbettungen implementieren
- modulepreload für kritische Module: Die wichtigsten Module im
<head>mitrel="modulepreload"deklarieren - Speculation Rules für Navigation: Wichtige Folgeseiten mit der Speculation Rules API vorab laden oder vorrendern
- Performance Budgets setzen: Feste Obergrenzen für Bundle-Größen in der CI/CD-Pipeline durchsetzen
- Kontinuierlich messen: Real User Monitoring mit der Web Vitals Library, Lighthouse CI in der Pipeline, regelmäßige Bundle-Audits
Fazit: Das schnellste JavaScript ist das, das Sie nicht ausliefern
Die JavaScript-Landschaft entwickelt sich rasant weiter. Mit Rolldown und Oxc stehen bahnbrechende Build-Tools vor der Tür, die Build-Zeiten um den Faktor 40 und mehr verkürzen können. Import Maps und die Speculation Rules API verändern, wie wir Module laden und Navigationen beschleunigen. Und die Anforderungen durch INP als Core-Web-Vital-Metrik zwingen uns, den gesamten JavaScript-Lebenszyklus zu überdenken.
Doch bei aller Technologie-Begeisterung gilt eine einfache Wahrheit: Das schnellste JavaScript ist das JavaScript, das Sie nicht ausliefern. Beginnen Sie jede Optimierung mit der Frage „Brauchen wir diesen Code wirklich?" und arbeiten Sie sich dann durch die hier vorgestellten Techniken. Die Kombination aus Bundle-Analyse, Tree Shaking, intelligentem Code-Splitting und bewusstem Umgang mit Third-Party-Scripts kann die JavaScript-Last Ihrer Website um 50 bis 70 Prozent reduzieren – mit direkten, messbaren Auswirkungen auf LCP, INP und die gesamte Nutzererfahrung.
Performance-Optimierung ist kein einmaliges Projekt, sondern ein fortlaufender Prozess. Setzen Sie Performance Budgets, automatisieren Sie Ihre Audits, und machen Sie JavaScript-Performance zu einem festen Bestandteil Ihres Entwicklungsworkflows. Denn am Ende geht es nicht um Lighthouse-Scores – es geht darum, dass Ihre Nutzer eine schnelle, reaktionsfreudige Website erleben, die sie gerne besuchen und immer wieder zurückkommen.