Skip to content
svelte-vitals
Esc
↑↓navigate↵open⌘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 (or SvelteKit 3’s $app/env; aliases included) or a typeof check such as typeof window !== 'undefined' or typeof matchMedia === 'function' (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 a literal ssr = false themselves, and the load functions of universal +page.ts/+layout.ts files whose route never renders on the server (their module scope is still checked: SvelteKit imports the file on the server when it cannot read the page options statically). A route never renders on the server when the nearest ssr export, looking first at the page’s own +page files and then up its layout chain (following +page@/+layout@ resets), is ssr = false. With kit.router.type: 'hash' in svelte.config.js, no route renders on the server. ssr = dev (with dev from $app/environment) counts as false, its value in every production build, though a file exporting it still has its module scope checked, since SvelteKit imports it on the server to read the option; an ssr exported as anything else counts as turning SSR back on. Where a node’s universal module (+page.ts/+layout.ts) and its server module both export it, the universal one wins, as in SvelteKit. A layout qualifies only when every page under it does, and the root layout only when it exports ssr = false itself. Server files (+page.server.ts, +server.ts, hooks.server.ts) and runes modules are still checked.

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'
  }
};