Prerendering ist die schnellste und günstigste Art, eine Seite auszuliefern: SvelteKit erzeugt sie schon beim Build als fertiges HTML. Beim Hosten musst du nur die Datei ausliefern — kein Server, kein Datenbank-Roundtrip, keine CPU-Last zur Laufzeit. Das macht Prerendering perfekt für Marketing-Seiten, Dokumentationen oder Blogs. Dieser Artikel zeigt im Detail, wie der Prerendering-Crawler funktioniert, wie du dynamische Routen vorab erzeugst und wo die Grenzen liegen.

Wie SvelteKit Routen findet, die prerendert werden sollen

Wenn du prerender = true setzt, weiß SvelteKit für diese eine Route, dass sie statisch werden soll. Aber woher weiß es, welche Slugs in /blog/[slug] existieren? Antwort: Es crawlt deine App.

So läuft der Crawler ab:

  1. SvelteKit startet bei der Wurzel-URL /.
  2. Es rendert die Seite zur Build-Zeit.
  3. Im fertigen HTML sucht es nach allen <a href="...">-Links.
  4. Jeden gefundenen Link folgt es nach — und das gleiche Spiel wiederholt sich.
  5. Am Ende hat es alle Routen erreicht, die irgendwo in der App verlinkt sind.

Das heißt: Wenn deine Blog-Übersicht Links zu allen Artikeln hat, werden alle Artikel prerendert. Wenn ein Artikel nirgends verlinkt ist, wird er übersehen — er muss zur Laufzeit gerendert werden.

Wenn der Crawler nicht reicht: die entries-Funktion

Bei manchen Apps sind nicht alle URLs verlinkt — etwa eine Privatsphären-Seite, die nur über die Footer-Navigation erreichbar ist (und der Footer kommt erst am Schluss). Oder eine Liste mit hunderten Einträgen, die paginiert ist und nicht alle gleich sichtbar sind.

Für solche Fälle exportierst du eine entries-Funktion in der +page.server.ts oder +page.ts der dynamischen Route. Sie sagt SvelteKit explizit: „Hier ist die Liste aller Slugs, die prerendert werden sollen.”

ts src/routes/blog/[slug]/+page.server.ts
import type { EntryGenerator } from './$types';
import { db } from '$lib/server/db';

export const prerender = true;

export const entries: EntryGenerator = async () => {
    const posts = await db.post.findMany({ select: { slug: true } });
    return posts.map((post) => ({ slug: post.slug }));
};

export async function load({ params }) {
    const post = await db.post.findUnique({ where: { slug: params.slug } });
    return { post };
}

entries läuft beim Build und liefert eine Liste von Parameter-Objekten. SvelteKit nutzt sie, um die [slug]-URL für jeden Wert auszufüllen und die zugehörige Seite zu prerenden — auch wenn nirgends ein Link existiert.

Was nicht prerenderbar ist

Damit eine Seite prerenderbar ist, muss sie deterministisch sein. Das heißt: Bei gleichem URL und gleichem Build-State muss immer das gleiche HTML rauskommen. Ein paar Dinge, die das verhindern:

Form Actions in der Page. Form Actions laufen zur Laufzeit. Eine Page mit Action kann nicht prerendert werden — du müsstest sie aufteilen.

Cookies oder Headers lesen im Load. Was vom Request abhängt, kann nicht beim Build feststehen. Wenn dein load cookies.get(...) macht, wirft SvelteKit einen Build-Fehler.

event.url.searchParams nutzen. Query-Parameter sind Teil der dynamischen URL. Wer auf ?page=2 reagiert, kann nicht prerendert werden — es sei denn, die spezifische Variante ist als entries deklariert.

Random- oder Datums-abhängige Logik. Math.random() oder new Date() zur Build-Zeit aufgerufen, gibt einen festen Wert zur Build-Zeit. Das ist meist nicht das, was du willst.

In all diesen Fällen ist die saubere Lösung: prerender = false für die problematische Route, oder die dynamische Logik in eine Komponente verschieben, die zur Laufzeit lebt.

Prerendered API-Endpunkte

Erstaunlicherweise kannst du auch +server.ts-Endpunkte prerendern. Klassischer Use Case: eine Sitemap.xml, die einmal beim Build erzeugt wird.

ts src/routes/sitemap.xml/+server.ts
import { db } from '$lib/server/db';

export const prerender = true;

export async function GET() {
    const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });

    const urls = posts.map((post) => `
        <url>
            <loc>https://example.com/blog/${post.slug}</loc>
            <lastmod>${post.updatedAt.toISOString()}</lastmod>
        </url>
    `).join('');

    return new Response(`
        <?xml version="1.0" encoding="UTF-8"?>
        <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
            ${urls}
        </urlset>
    `, {
        headers: { 'Content-Type': 'application/xml' },
    });
}

SvelteKit ruft den Endpoint beim Build genau einmal auf, speichert die Antwort als statische Datei und liefert sie ab da bei jedem Request aus.

Genauso lässt sich ein RSS-Feed prerendern, eine robots.txt mit dynamischem Inhalt, oder eine JSON-Manifest-Datei.

Konfiguration: was passiert, wenn was schiefgeht?

In svelte.config.js kannst du das Verhalten des Prerenders feinjustieren:

js svelte.config.js
export default {
    kit: {
        prerender: {
            crawl: true,                       // Crawler aktiv (Default)
            entries: ['*'],                    // alle prerender=true-Seiten
            handleHttpError: 'warn',           // statt 'fail'
            handleMissingId: 'warn',           // bei fehlenden Hash-Anchors
            handleEntryGeneratorMismatch: 'warn',
        },
    },
};

Die wichtigste Option ist handleHttpError. Standard: 'fail' — der Build bricht ab, wenn der Crawler eine 404 findet. Das ist meist gewollt (defekte Links sind ein echtes Problem). Manchmal hast du aber bewusst Links auf nicht prerenderbare Routen — dann setzt du 'warn' oder 'ignore'.

Mit einer Funktion kannst du differenzieren:

js Selektive Fehlerbehandlung
prerender: {
    handleHttpError: ({ path, referrer, message }) => {
        if (path === '/admin') return; // ignorieren
        throw new Error(message);
    },
},

Wann lohnt sich Prerendering?

InhaltstypEmpfehlung
Marketing-Seite, Landing-PagePrerender
Blog mit nicht-zu-vielen ArtikelnPrerender + entries
Dokumentation, How-To-InhaltePrerender
Open-Graph-Bilder, Sitemap, RSSPrerender
Produkt-Detail-Seiten in einem ShopPrerender oder ISR
Dashboard mit eingeloggten DatenNiemals prerendern (SSR oder CSR)
News-Feed, der jede Minute updatedSSR mit Cache-Control
Live-Suche, Live-DatenSSR oder CSR

Faustregel: Inhalte, die sich selten ändern und keine Nutzer-Personalisierung brauchen, sind prerender-Kandidaten.

Inkrementelle Builds vs. komplette Neu-Generierung

Bei sehr großen Sites (zehntausende Blog-Artikel) wird der Build langsam, weil SvelteKit bei jedem Deploy alles neu prerendert. Zwei Auswege:

Adapter mit ISR (Incremental Static Regeneration). Adapter wie der Vercel-Adapter erlauben, dass Seiten zur Laufzeit generiert und dann gecached werden. Beim nächsten Request kommt die gecachte Version, nach Ablauf einer Frist wird sie aktualisiert.

ts ISR pro Route
export const config = {
    isr: {
        expiration: 60, // alle 60 Sekunden neu generieren
    },
};

Selektives Prerendering. Du prerenderst nur die wichtigsten Seiten (z. B. die letzten 100 Artikel) und lässt den Rest zur Laufzeit per SSR rendern. Das geht über die entries-Funktion mit Filter.

Bei wirklich großen Sites kommen oft beide Strategien zum Einsatz: Static-First für die populären URLs, SSR-mit-Cache für den Long-Tail.

Besonderheiten beim Prerendering

Ein paar Eigenheiten, die nicht offensichtlich sind:

Der Crawler folgt auch Links in <svelte:head>. Wenn du in <svelte:head> einen <link rel="alternate"> setzt, folgt der Crawler ihm — manchmal hilfreich, manchmal lästig. Bei externen Links setze data-sveltekit-noscroll-ähnliche Hinweise.

Prerendered Seiten haben echtes HTML — auch ohne JavaScript komplett funktional. Wenn du csr=false zusammen mit prerender=true setzt, erzeugst du eine reine HTML-Datei ohne jegliches JavaScript-Bundle. Schnellster denkbarer Page-Load.

Der Build-Output landet in build/ (mit adapter-static) oder einer adapter-spezifischen Struktur. Der adapter-static produziert ein normales build/-Verzeichnis mit allen HTML-Dateien — du kannst es 1:1 auf jeden Web-Server kopieren. Andere Adapter packen es in deren Format ein.

Prerender-Fehler sind oft Hydration-Fehler im Verkleidung. Wenn der Build mit einem mysteriösen Fehler abbricht, ist es manchmal eine Komponente, die window zur Render-Zeit nutzt. Die schlägt beim Build fehl, weil dort kein window existiert. Lösung: Den Code in einen $effect oder onMount packen, der erst im Browser läuft.

Layout-Optionen werden vererbt — auch beim Prerender. Wenn du prerender=true im Root-Layout setzt, müssen alle Sub-Routen prerenderbar sein. Eine einzelne Route mit Form Action wirft einen Build-Fehler. Lösung: Die Form-Action-Route mit eigenem prerender=false überschreiben.

Prerender-Daten werden als JSON neben dem HTML serialisiert. Das HTML zeigt schon den Inhalt. Damit aber Hydration im Browser funktioniert, packt SvelteKit die Daten zusätzlich in einen Script-Tag. Das macht prerendered Pages etwas größer als reine Server-Render — Trade-off zugunsten schneller Hydration.

Externe Links werden nicht gecrawlt. Der Crawler folgt nur internen Links. Wenn du eine /redirect/<external>-Route prerendern willst, brauchst du entries mit den Listen.

Prerendered Pages umgehen den handle-Hook nicht — beim Build. Beim Build läuft die ganze Stack: handle, load, +page.svelte. Was im handle als event.locals.user = null gesetzt wird, ist auch beim Build der Fall (außer du kannst irgendwie einen User für den Build vortäuschen). Das ist wichtig: Geprerenderte Seiten zeigen immer den anonymen Zustand.

Weiterführende Ressourcen

Externe Quellen

Verwandte Artikel

/ Weiter

Zurück zu SvelteKit Rendering

Zur Übersicht