Skip to content
svelte-vitals
English
Esc
navigateopen⌘Jpreview
On this page

correctness/server-browser-global · Browser global in server module code

window, document, localStorage accessed in module scope or a load/handler crash SSR with a ReferenceError.

Severity: critical · Category: correctness

What it checks

Flags reads of browser-only globals (window, document, localStorage, sessionStorage, navigator, location, history, screen, matchMedia, requestAnimationFrame, cancelAnimationFrame, IntersectionObserver, ResizeObserver, MutationObserver, alert, confirm, prompt) in code that always runs on the server:

  • module scope of a .svelte.ts/.svelte.js runes module or a .svelte <script module> block (crashes when the module is imported on the server), and
  • SvelteKit route and hooks files: top level, load/action/endpoint handler bodies, and the init hook (crashes at import or on every request).

Not flagged:

  • Code guarded by browser from $app/environment (aliases included) or a typeof window !== 'undefined' check (early-return guards included).
  • Code inside onMount, $effect, or ordinary functions, since none of them run at module evaluation.
  • A bare typeof window, which never throws.
  • Names you imported or declared yourself (const document = …).
  • Closures nested inside handlers, typically client callbacks.
  • Files that export ssr = false themselves.

Why it matters

None of these globals exist in Node. A module-scope window read crashes the server the moment the file is imported; in a load it crashes every SSR request with ReferenceError: window is not defined, a production 500 the compiler never warns about.

How to fix

export function load() {
  const stored = localStorage.getItem('filters'); // ❌ ReferenceError on the server

  return {};
}

Move the browser access to the client side, into onMount, which never runs on the server:

<script>
  import { onMount } from 'svelte';

  let stored = $state(null);
  onMount(() => {
    stored = localStorage.getItem('filters'); // ✅ onMount never runs on the server
  });
</script>

Or guard it explicitly:

import { browser } from '$app/environment';

const stored = browser ? localStorage.getItem('filters') : null; // ✅

Mode differences

None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.

Disabling

Silence a single occurrence with <!-- svelte-vitals-disable-next-line correctness/server-browser-global --> on the line above it, or turn the rule off:

export default {
  rules: {
    'correctness/server-browser-global': 'off'
  }
};