„window is not defined“ in servergerenderten Apps beheben
Beheben Sie window is not defined in servergerenderten Apps mit On-Mount-Hooks, typeof-window-Checks und Client-only-Imports für Abhängigkeiten.
Der Fehler window is not defined bedeutet, dass Ihr Code in Node.js ausgeführt wurde, wo kein window-Objekt existiert: Server-Rendering-Frameworks führen Ihre Komponenten zuerst auf dem Server aus, noch bevor überhaupt ein Browser beteiligt ist.
Der Fehler tritt üblicherweise direkt dann auf, wenn Sie einer funktionierenden App Server-Rendering hinzufügen oder eine Komponente, die clientseitig einwandfrei lief, nach Next, Nuxt, SvelteKit, Astro oder React Router verschieben. Die Komponente hat sich nicht verändert – wohl aber der Ort, an dem sie ausgeführt wird. Und der Stacktrace verrät Ihnen, welche der drei folgenden Lösungen Sie brauchen.
Die wichtigsten Erkenntnisse
window is not definedbedeutet, dass der Code in Node.js gelaufen ist, wowindowzu keinem Zeitpunkt in irgendeinem Lifecycle existiert; es handelt sich nicht um ein Timing-Problem.- Die Standardlösung besteht darin, den Zugriff in einen On-Mount-Hook zu verlagern (
useEffect,onMounted,onMount), da Mount-Hooks niemals auf dem Server ausgeführt werden. - Ein
typeof window !== 'undefined'-Guard gehört in Code auf Modulebene und in gemeinsam genutzte Utilities; innerhalb des Renderings einer Komponente führt er dazu, dass Server- und Client-HTML voneinander abweichen. - Client-only-Rendering ist die letzte Option: Es entfernt die Komponente vollständig aus dem HTML des Servers.
- Derselbe Absturz kann auch während des Builds auftreten, denn die statische Generierung führt Komponenten in Node aus, um HTML zu erzeugen.
Warum tritt „window is not defined“ in servergerenderten Apps auf?
Servergerenderte Apps führen Ihre Komponenten zweimal aus: einmal in Node.js, um HTML zu erzeugen, und dann erneut im Browser. Der globale Scope von Node.js enthält weder window noch document. Jeder Code, der während des Server-Durchlaufs darauf zugreift, löst daher einen ReferenceError aus. Das Objekt ist nicht „noch nicht verfügbar“ – in Node existiert es überhaupt nicht.
function ThemeBadge() {
// ReferenceError: window is not defined (thrown during the server render)
const theme = window.localStorage.getItem('theme');
return <span>{theme}</span>;
}
Das Gleiche gilt auch ohne jeden Request. Die statische Generierung führt Ihre Komponenten zur Build-Zeit in Node aus, um HTML zu erzeugen. Ein window-Zugriff kann also während next build oder beim Prerendering fehlschlagen, wobei der Stacktrace dann in der Build-Ausgabe erscheint und nicht in einem Server-Log. SvelteKit macht diese Phase sogar über die building-Konstante zugänglich, die während des Prerenderings true ist. Eine Komponente, die in der Entwicklung ausschließlich clientseitig gerendert wird, kann deshalb lokale Tests bestehen und trotzdem den Production-Build zerstören.
Was, wenn der Absturz in einer Dependency liegt?
Wenn die obersten Frames des Stacktrace in node_modules verweisen, liest eine Dependency window zum Import-Zeitpunkt aus – und der Fehler tritt auf, bevor überhaupt Code Ihrer Komponenten läuft. Charting-Bibliotheken, Embed-SDKs und alles, was auf Modulebene das DOM abfragt, sind die üblichen Verdächtigen.
ReferenceError: window is not defined
at node_modules/some-chart-lib/dist/index.js:12:3
at Module._compile (node:internal/modules/cjs/loader:1358:14)
Diese Unterscheidung bestimmt die Lösung. Ein Fehler zum Import-Zeitpunkt wird beim Laden des Moduls ausgelöst, sodass das Verpacken Ihrer eigenen Zugriffe in einen On-Mount-Hook nichts hilft; der Absturz passiert, bevor die Komponente existiert. Für solche Pakete springen Sie direkt zum Client-only-Import in Lösung drei.
Lösung 1: Den Zugriff in einen On-Mount-Hook verlagern
Die Standardlösung besteht darin, den window-Zugriff in den On-Mount-Hook Ihres Frameworks zu verlagern, denn Mount-Hooks werden ausschließlich im Browser ausgeführt. Die useEffect-Referenz von React ist hier eindeutig: Der Server-Render überspringt Effects, und sie feuern erst, sobald die Komponente im Browser angekommen ist. Die Äquivalente: Vue und Nuxt nutzen onMounted, Svelte und SvelteKit nutzen onMount, das eine auf dem Server gerenderte Komponente nie aufruft, React Router nutzt Reacts useEffect, und in Astro-Komponenten gehört Browser-Code in die Lifecycle-Hooks einer Framework-Island.
import { useState, useEffect } from 'react';
function ThemeBadge() {
const [theme, setTheme] = useState(null);
useEffect(() => {
setTheme(window.localStorage.getItem('theme')); // browser only
}, []);
return <span>{theme ?? 'default'}</span>;
}
Der Server rendert den Fallback-Zustand, der Browser mountet, der Effect läuft, und der echte Wert wird eingesetzt. Damit bleibt das Server-HTML für den Rest der Komponente unangetastet – genau deshalb ist dieser Ansatz den beiden anderen Lösungen als Standard vorzuziehen.
Lösung 2: Mit typeof window !== ‘undefined’ abschirmen
Ein typeof window !== 'undefined'-Guard ist das richtige Werkzeug für Code auf Modulebene und für gemeinsam genutzte Utilities, wo kein Lifecycle-Hook zur Verfügung steht.
// theme.js — a shared utility, no component lifecycle to lean on
export function getStoredTheme() {
if (typeof window === 'undefined') return 'light'; // server fallback
return window.localStorage.getItem('theme') ?? 'light';
}
SvelteKit bietet mit der browser-Konstante eine elegantere Entsprechung, und die FAQ zu clientseitigen Bibliotheken behandelt diese Konstante als Standardweg, um alles abzuschirmen, was auf document oder window zugreift.
Innerhalb des Renderings einer Komponente ist der Guard hingegen die falsche Wahl: Er führt dazu, dass Server und Browser für dieselbe Komponente unterschiedliches HTML erzeugen – Sie tauschen also einen Absturz gegen einen Mismatch, sobald der Client übernimmt. Behalten Sie den Guard in einfachen Funktionen und auf Modulebene; innerhalb von Komponenten nutzen Sie Lösung eins.
Lösung 3: Server-Rendering für die Komponente überspringen
Die letzte Option ist ein Client-only-Dynamic-Import, der die Komponente vollständig vom Server-Rendering ausnimmt. In Next.js erledigt next/dynamic mit ssr: false das innerhalb einer Client Component (in Server Components führt es zu einem Fehler, also fügen Sie einen schlanken 'use client'-Wrapper hinzu). Nuxt bietet <ClientOnly>, Astro die client:only-Direktive.
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
ssr: false,
loading: () => <div style={{ height: 320 }} aria-hidden="true" />,
});
Machen Sie sich die Kosten klar, bevor Sie darauf zurückgreifen: Der Server liefert für diesen Teilbaum kein HTML, die Komponente fehlt also im initialen HTML – was dem SEO schaden und die Interaktivität verzögern kann. Reservieren Sie das für Komponenten, die Sie nicht ändern können, in erster Linie Dependencies, die zum Import-Zeitpunkt einen Fehler werfen.
Das Pop-in mit einem formgleichen Placeholder vermeiden
Ein Placeholder verhindert Layout-Shifts nur dann, wenn er dieselben Abmessungen einnimmt wie die Komponente, für die er einsteht. Wenn auf dem Server null gerendert wird, erscheint die Komponente aus dem Nichts, sobald JavaScript läuft, und schiebt alles darunter die Seite hinunter. Ein Skeleton mit festem Platzbedarf – wie das 320px-div oben – hält den Raum frei, bis das echte Markup eintrifft. Ob überhaupt ein Placeholder oder doch null gerendert werden sollte, ist derselbe Trade-off, der hinter vielen Hydration-Mismatches steckt; unser Leitfaden zum Beheben von Next.js-Hydration-Fehlern behandelt das ausführlich. In Session Replays von Client-only-Fallbacks wird der Wechsel von Placeholder zu Inhalt als Layout-Sprung sichtbar – der schnellste Weg zu prüfen, ob ein Placeholder tatsächlich zum Markup passt, das er ersetzt.
Welche Lösung passt zu Ihrem Fall?
- Ihre Komponente liest
windowim eigenen Code: Verlagern Sie den Zugriff in den On-Mount-Hook. Die Standardwahl. - Eine gemeinsam genutzte Utility oder ein Statement auf Modulebene greift auf
windowzu: Fügen Sie dentypeof window-Guard mit einem Server-Fallback-Wert hinzu. - Der Stacktrace verweist zum Import-Zeitpunkt in
node_modules: Client-only-Dynamic-Import, mit einem formgleichen Placeholder. - Der Fehler erscheint nur in der Build-Ausgabe: dieselbe Triage wie oben; die statische Generierung führt exakt denselben Codepfad in Node aus.
Lesen Sie zuerst den Stacktrace
Der Fehler ist ein Umgebungsproblem, kein Timing-Problem: Irgendeine Codezeile wurde in Node ausgeführt, wo window nie existiert hat. Lesen Sie zuerst den Stacktrace. Wenn der oberste Frame aus Ihrem Code stammt, beheben ein On-Mount-Hook oder ein Guard das Problem und erhalten dabei das Server-HTML. Verweist er in node_modules, isolieren Sie die Dependency hinter einem Client-only-Import und geben ihr einen Placeholder, der das Layout hält.
FAQs
Ist „document is not defined“ dasselbe Problem wie „window is not defined“?
Ja. Beide Fehler haben dieselbe Ursache: Der Code wurde in Node.js ausgeführt, dessen globaler Scope weder window noch document enthält. Dieselbe Triage und dieselben drei Lösungen greifen also – verlagern Sie den Zugriff in einen On-Mount-Hook, schirmen Sie Code auf Modulebene mit einer typeof-Prüfung ab, oder rendern Sie die Komponente client-only, wenn eine Dependency zum Import-Zeitpunkt auf das DOM zugreift.
Kann ich den Fehler beheben, indem ich auf dem Server ein globales window-Objekt definiere?
Besser nicht. Ein gefälschtes window an globalThis zuzuweisen bringt den ReferenceError zum Schweigen, aber der Server rendert dann Markup aus Fake-Werten, und alles, was im Polyfill gespeichert wird, ist über sämtliche Requests hinweg geteilt, die der Server verarbeitet. Außerdem verdeckt es Import-Zeit-Abstürze in Dependencies, statt sie sichtbar zu machen. Verlagern Sie den Zugriff stattdessen in einen On-Mount-Hook oder hinter einen typeof window-Guard.
Warum erscheint „window is not defined“ in Next.js weiterhin, obwohl ssr: false gesetzt ist?
Dafür gibt es zwei häufige Gründe. Im App Router akzeptiert next/dynamic ssr: false nur aus einer Client Component heraus, und Next.js wirft einen Fehler, wenn die Option in einer Server Component auftaucht – verpacken Sie sie also in eine schlanke 'use client'-Komponente. Außerdem betrifft ssr: false nur diesen einen Dynamic-Import: Wenn eine andere, auf dem Server ausgeführte Datei dieselbe Bibliothek statisch importiert, läuft ihr window-Zugriff auf Modulebene weiterhin in Node.
Existiert localStorage in Node.js?
Teilweise. Node liefert seit v22.4.0 ein globales localStorage aus, ab v25.0.0 ohne Flag, das bis zu 10 MB in der über das Flag --localstorage-file übergebenen Datei persistiert; in v26 wirft der Zugriff ohne dieses Flag eine DOMException. Auf einem Server steht dahinter ein einziger Store für den gesamten Prozess, nicht einer pro Besucher oder pro Request – es hat also nichts mit dem nutzerspezifischen Storage des Browsers zu tun. Und window.localStorage wirft weiterhin einen Fehler, weil window selbst in Node nie existiert.