Skip to content
svelte-vitals
Esc
↑↓navigate↵open⌘Jpreview
On this page

correctness/instance-browser-global · Browser global during component initialisation

A component's instance script runs on the server during SSR, so window/document reads at its top level crash the render.

Severity: warning · Category: correctness

What it checks

Flags reads of browser-only globals (the same list as correctness/server-browser-global) at the top level of a component’s <script>, since that code runs on the server on every SSR render of the component. The same guards apply: browser from $app/environment (or $app/env), typeof checks (early-return guards included), onMount/$effect bodies, your own bindings (a legacy component’s $: name = … included), and shadowed locals are never flagged.

Why it matters

const width = window.innerWidth; at the top of a component works in the browser and in a client-only dev flow, then crashes the first SSR render with ReferenceError: window is not defined.

This is a warning, not critical: a component that is only ever rendered behind a parent’s {#if browser} (or dynamically imported on the client) legitimately never runs on the server, and that cannot be proven from the component file alone. If that is your case, add // svelte-vitals-disable-next-line correctness/instance-browser-global above the line.

Nothing is flagged in a +page/+layout component whose route never renders on the server. 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; 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. Other components, such as shared ones under src/lib, are skipped only when SSR is off app-wide: the hash router, or a root src/routes/+layout.ts (or +layout.server.ts) exports ssr = false and no other +page/+layout file exports ssr with any other value.

A browser global used only as a $props() destructuring default (let { width = window.innerWidth } = $props()) is not flagged either. The default only evaluates when the prop is absent, including during SSR, but the scanner doesn’t visit destructuring defaults. This is a silent conservative miss, not a guard.

How to fix

<script>
  const width = window.innerWidth; // ❌ crashes SSR
</script>

The preferred modern form for a window property is svelte/reactivity/window (5.11.0+): no guard needed, undefined on the server, reactive on the client:

<script>
  import { innerWidth } from 'svelte/reactivity/window';
</script>

<p>{innerWidth.current}</p>

For anything svelte/reactivity/window doesn’t cover, read the global in onMount, which never runs on the server:

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

  let width2 = $state(0);
  onMount(() => {
    width2 = window.innerWidth; // ✅ onMount never runs on the server
  });
</script>

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/instance-browser-global --> on the line above it, or turn the rule off:

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