← Blog | Performance SEO

JavaScript-Performance für SPAs: Bundle-Größen und Code-Splitting

Große JavaScript-Bundles sind einer der häufigsten Gründe für schlechte Core Web Vitals in modernen Web-Apps. So reduzierst du Bundle-Größen mit Code-Splitting, Tree Shaking und Lazy Loading.

Shift07 Team 12 Min. Lesezeit
JavaScript-Performance für SPAs: Bundle-Größen optimieren und Code-Splitting

Single-Page-Applications (SPAs) — gebaut mit React, Vue, Angular oder Svelte — bieten hervorragende Nutzererfahrungen. Aber sie bringen eine ernsthafte SEO- und Performance-Herausforderung mit sich: riesige JavaScript-Bundles. Wenn ein Nutzer deine SPA öffnet, muss der Browser erst viele Hundert Kilobyte (manchmal Megabyte) JavaScript herunterladen, parsen und ausführen — bevor irgendetwas auf dem Bildschirm erscheint.

Das schlägt sich direkt in den Core Web Vitals nieder: ein hoher LCP (Largest Contentful Paint) und schlechter INP (Interaction to Next Paint). Google bewertet diese Werte als Ranking-Faktor. In diesem Artikel zeige ich dir, wie du systematisch vorgehst.

Warum Bundle-Größe für SEO wichtig ist: Google crawlt JavaScript-lastige Seiten mit Verzögerung. Zu große Bundles können dazu führen, dass Googlebot wichtige Inhalte nicht oder zu spät rendert — und deine Seiten schlechter indexiert. Lies dazu auch unseren Artikel zu JavaScript SEO: Wie Google SPA-Websites crawlt.

Das Problem: Warum SPAs langsam werden

Ein typisches React-Projekt ohne Optimierung bündelt alles in eine einzige Datei: main.js oder bundle.js. Darunter fallen:

Ein mittelgroßes React-Projekt landet schnell bei 500 KB – 1,5 MB unkomprimiert. Selbst mit Gzip sind das noch 150–400 KB Transfer. Das kostet Zeit — besonders auf mobilen Verbindungen.

Der Browser-Prozess läuft in drei Phasen:

  1. Download: Die Datei muss übertragen werden
  2. Parse: JavaScript parsen ist CPU-intensiv — besonders auf schwachen Smartphones
  3. Execute: Der Code wird ausgeführt, die App initialisiert

Erst danach ist die Seite interaktiv. All diese Zeit erscheint dem Nutzer (und Google) als Ladezeit.

Lösung 1: Code-Splitting — Teile und herrsche

Code-Splitting bedeutet: statt einen großen Bundle auszuliefern, teilst du den Code in viele kleine Chunks auf. Der Browser lädt nur, was er gerade braucht.

Route-basiertes Code-Splitting

Die wirksamste Methode: jede Route (Seite) bekommt ihren eigenen Chunk. So lädt ein Nutzer, der nur die Startseite besucht, nicht den Code für den Admin-Bereich oder die Checkout-Seite.

React mit React.lazy() und Suspense:

// Vorher: alles importiert (blockiert den initialen Load)
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ProductPage from './pages/ProductPage';
import CheckoutPage from './pages/CheckoutPage';

// Nachher: lazy imports (werden erst bei Bedarf geladen)
const HomePage = React.lazy(() => import('./pages/HomePage'));
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
const ProductPage = React.lazy(() => import('./pages/ProductPage'));
const CheckoutPage = React.lazy(() => import('./pages/CheckoutPage'));

// In der App-Komponente:
function App() {
  return (
    <Suspense fallback={<div>Lädt...</div>}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/produkt/:id" element={<ProductPage />} />
        <Route path="/checkout" element={<CheckoutPage />} />
      </Routes>
    </Suspense>
  );
}

Vue mit defineAsyncComponent:

// Vue 3 — async component
const CheckoutPage = defineAsyncComponent(() =>
  import('./pages/CheckoutPage.vue')
);

// In vue-router:
const routes = [
  {
    path: '/checkout',
    component: () => import('./pages/CheckoutPage.vue')
  }
];

Angular mit loadComponent (seit Angular 15+):

// app.routes.ts
export const routes: Routes = [
  {
    path: 'checkout',
    loadComponent: () =>
      import('./checkout/checkout.component').then(m => m.CheckoutComponent)
  }
];

Praxis-Tipp: Prüfe den Effekt mit dem Chrome DevTools Network-Tab. Öffne die App, wechsle die Route und beobachte, ob neue JavaScript-Chunks nachgeladen werden. Wenn ja, funktioniert Code-Splitting korrekt.

Lösung 2: Tree Shaking — toten Code entfernen

Tree Shaking ist der Prozess, bei dem der Bundle-Builder (Webpack, Vite, Rollup) nicht verwendeten Code aus dem finalen Bundle entfernt. Es funktioniert mit ES-Modules (import/export) — nicht mit CommonJS (require).

Das häufigste Problem: falsche Imports

// ❌ Schlechter Import — zieht die GESAMTE Lodash-Bibliothek rein (70+ KB)
import _ from 'lodash';
const result = _.groupBy(items, 'category');

// ✅ Guter Import — nur die eine Funktion (paar KB)
import groupBy from 'lodash/groupBy';
const result = groupBy(items, 'category');

// Noch besser: lodash-es (ESM-Version, besser tree-shakeable)
import { groupBy } from 'lodash-es';
const result = groupBy(items, 'category');

Das gilt für fast alle großen Libraries:

Tree Shaking in Vite / Webpack sicherstellen

Vite tree-shakt automatisch. Bei Webpack prüfe die Konfiguration:

// webpack.config.js — sicherstellen dass mode=production aktiv ist
module.exports = {
  mode: 'production',   // aktiviert Tree Shaking + Minifizierung
  optimization: {
    usedExports: true,  // markiert ungenutzte Exports
    minimize: true,     // entfernt toten Code
  }
};

Außerdem: Prüfe die package.json deiner Dependencies. Pakete die "sideEffects": false setzen, sind besonders gut tree-shakeable. Wenn dein eigenes Paket keine Seiteneffekte hat, setze dieses Flag ebenfalls.

Lösung 3: Dynamische Imports für schwere Komponenten

Manche Komponenten sind schwer, werden aber nur selten gebraucht: ein Diagramm-Editor, eine PDF-Export-Funktion, ein Rich-Text-Editor. Mit dynamischen Imports lädst du sie erst wenn der Nutzer sie tatsächlich anfordert.

// Ein schweres Chart-Library — nur laden wenn die Komponente aktiv genutzt wird
function DashboardPage() {
  const [chart, setChart] = useState(null);

  async function loadChart() {
    // chart.js ist ~200 KB — erst beim Klick laden
    const { Chart } = await import('chart.js');
    const { default: ChartComponent } = await import('./ChartComponent');
    setChart(() => ChartComponent);
  }

  return (
    <div>
      <button onClick={loadChart}>Diagramm anzeigen</button>
      {chart && <Chart />}
    </div>
  );
}

Achtung: Dynamische Imports erzeugen einen kurzen Ladeflash wenn der Chunk nachgeladen wird. Zeige immer einen Lade-Indikator (Suspense Fallback). Nutzer erwarten Feedback — eine kurze Verzögerung ist akzeptabel, ein stummer Hänger nicht.

Lösung 4: Bundle-Analyse — wo sitzt das Problem?

Bevor du optimierst, musst du wissen, was deinen Bundle aufbläht. Zwei unverzichtbare Tools:

webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer

# In webpack.config.js:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
  plugins: [new BundleAnalyzerPlugin()]
};

# Oder als CLI:
npx webpack-bundle-analyzer build/static/js/*.js

Vite mit rollup-plugin-visualizer

npm install --save-dev rollup-plugin-visualizer

# vite.config.js:
import { visualizer } from 'rollup-plugin-visualizer';
export default {
  plugins: [
    visualizer({
      open: true,        // öffnet automatisch nach Build
      gzipSize: true,    // zeigt Gzip-Größe
      brotliSize: true,  // zeigt Brotli-Größe
    })
  ]
};

Das Ergebnis ist eine interaktive Treemap: du siehst sofort, welche Pakete wie viel Platz belegen. Häufige Überraschungen: moment.js mit seinen Locale-Files (>200 KB), ein ungenutztes Icon-Pack, oder eine mehrfach gebündelte Dependency.

Lösung 5: Vendor-Splitting und Long-Term-Caching

Eine oft übersehene Optimierung: trenne deine eigene App-Code von stabilen Drittanbieter-Libraries. Warum? Weil sich React oder Lodash selten ändern — aber dein App-Code bei jedem Deploy.

// vite.config.js — manuelle Chunk-Aufteilung
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Stabile Libraries in eigenen Chunk packen
          vendor: ['react', 'react-dom', 'react-router-dom'],
          ui: ['@mui/material', '@emotion/react'],
          utils: ['lodash-es', 'date-fns'],
        }
      }
    }
  }
};

Der Vorteil: Nutzer, die deine App zum zweiten Mal besuchen, laden vendor.js aus dem Browser-Cache — nicht neu vom Server. Nur dein App-Code (der sich häufig ändert) muss neu geladen werden. Das verkürzt den Reload deutlich.

Prüfe die Cache-Strategie mit unserem HTTP-Header-Checker — der zeigt dir ob Cache-Control korrekt gesetzt ist.

Lösung 6: Preloading und Prefetching

Code-Splitting verzögert das Laden von Chunks — aber du kannst diese Verzögerung kaschieren, indem du Chunks vorsorglich vorlädst bevor der Nutzer sie braucht.

Prefetch für wahrscheinliche nächste Schritte

// React Router mit prefetch-Hint
// Wenn Nutzer auf der Produktliste ist — Checkout wahrscheinlich als nächstes
const CheckoutPage = React.lazy(
  () => import(/* webpackPrefetch: true */ './pages/CheckoutPage')
);

// Vite-Äquivalent mit magischem Kommentar:
const CheckoutPage = React.lazy(
  () => import(/* @vite-ignore */ './pages/CheckoutPage')
  // alternativ: manuelle <link rel="prefetch"> im HTML
);
<!-- Im HTML-Head: Chunk vorsorglich laden -->
<link rel="prefetch" href="/assets/checkout-chunk.js" as="script">

<!-- Für kritische Ressourcen: preload (höhere Priorität) -->
<link rel="preload" href="/assets/main-chunk.js" as="script">

Der Unterschied: preload lädt mit hoher Priorität für den aktuellen Seitenaufruf, prefetch lädt im Hintergrund für wahrscheinliche nächste Seiten — in der Idle-Zeit des Browsers.

Benchmarking: Vor und nach der Optimierung

Messe den Fortschritt konkret:

  1. Chrome DevTools → Network-Tab: Aktiviere "Disable cache", lade die Seite neu, notiere die übertragene JS-Gesamtgröße
  2. Lighthouse Audit: Führe vor und nach der Optimierung einen Lighthouse-Audit durch — besonders auf "Reduce unused JavaScript"
  3. Core Web Vitals Checker: Nutze unseren Core Web Vitals Checker für die Echtmessung via PageSpeed Insights API

Typische Ergebnisse nach einer Optimierungsrunde:

Performance-Budget festlegen: Definiere ein JavaScript-Budget für dein Projekt — z.B. "initial bundle unter 200 KB gzipped". Vite und Webpack können bei Überschreitung warnen: build.chunkSizeWarningLimit: 200 in der Vite-Konfiguration.

JavaScript-Performance und Crawlability

Ein wichtiger SEO-Aspekt, der oft vergessen wird: Googlebot hat ein begrenztes Crawl-Budget. Wenn das JavaScript-Rendering deiner SPA zu ressourcenintensiv ist, kann Googlebot Seiten unvollständig rendern oder gar nicht erst in die Rendering-Queue aufnehmen.

Prüfe mit unserem JavaScript SEO Checker, ob kritische Inhalte auf deinen Seiten ohne JavaScript sichtbar sind — oder ob du auf Dynamic Rendering oder SSR (Server-Side Rendering) setzen solltest.

Große JavaScript-Bundles verlangsamen übrigens nicht nur Nutzer — sie verlangsamen auch den Renderingprozess von Googlebot selbst. Kleinere, aufgeteilte Bundles sind daher nicht nur für Nutzer besser, sondern verbessern auch die Indexierbarkeit.

Fazit: Schritt-für-Schritt-Plan

  1. Analysiere zuerst: webpack-bundle-analyzer oder rollup-plugin-visualizer zeigen dir, wo die größten Blöcke liegen
  2. Route-Splitting implementieren: React.lazy(), defineAsyncComponent() oder Angular loadComponent — je nach Framework
  3. Imports bereinigen: Prüfe lodash, moment, Icon-Libraries — importiere nur was du brauchst
  4. Vendor-Chunk trennen: Stabile Dependencies in separaten Chunk für besseres Caching
  5. Messen: Core Web Vitals vor und nach dem Deployment vergleichen
  6. Budget setzen: chunkSizeWarningLimit verhindert künftigen Rückfall

JavaScript-Performance-Optimierung ist kein einmaliges Projekt, sondern ein Prozess. Mit jedem neuen npm-Paket kann sich der Bundle wieder aufblähen — deshalb ist ein festes Performance-Budget und ein regelmäßiges Audit unverzichtbar.

Verwandte Artikel

Tech-SEO
JavaScript SEO: Wie Google SPA-Websites crawlt
Indexierung von Single-Page-Apps erklärt
Tech-SEO
JavaScript und Crawl-Budget: Wie JS Googlebot beeinflusst
Crawl-Budget verstehen und optimieren
Performance
Core Web Vitals verbessern: LCP, CLS, INP
Schritt-für-Schritt-Anleitung
Performance
Critical CSS: Above-the-Fold für besseres LCP
Critical CSS extrahieren und inline einbinden

Performance deiner SPA prüfen

Nutze unsere kostenlosen Tools, um die JavaScript-Performance deiner Website zu analysieren: