コンテンツにスキップ
svelte-vitals
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

correctness/server-browser-global · サーバー実行モジュールコードでの browser global

モジュールスコープや load・ハンドラで window や document、localStorage を参照すると、SSR が ReferenceError で落ちます。

重大度: critical · カテゴリ: correctness

チェック内容

必ずサーバーで実行されるコードでの browser 専用 global(window、document、localStorage、sessionStorage、navigator、location、history、screen、matchMedia、requestAnimationFrame、cancelAnimationFrame、IntersectionObserver、ResizeObserver、MutationObserver、alert、confirm、prompt)の読み取りを検出します。

  • .svelte.ts/.svelte.js runes モジュールや .svelte の <script module> ブロックのモジュールスコープ(サーバーで import された瞬間にクラッシュ)
  • SvelteKit のルート/フックファイル:トップレベル、load/action/エンドポイント handler 本体、init フック(import 時またはリクエストごとにクラッシュ)

検出対象外:

  • $app/environment(SvelteKit 3 では $app/env)の browser(エイリアス込み)や、typeof window !== 'undefined'、typeof matchMedia === 'function' のような typeof チェックでガードされたコード(early-return ガードを含む)。
  • onMount/$effect/通常の関数内。モジュール評価時には実行されません。
  • 裸の typeof window(throw しない)。
  • 自分で import/宣言した名前(const document = …)。
  • handler 内にネストしたクロージャ(典型的にはクライアント側コールバック)。
  • 自身がリテラルの ssr = false を export するファイル。ルートがサーバーでレンダリングされない universal な +page.ts/+layout.ts の load 関数も対象外です(モジュールスコープは引き続き検査します。ページオプションを静的に読めないとき、SvelteKit はそのファイルをサーバーで import するためです)。ページ自身の +page ファイル、続いてレイアウトチェーン(+page@/+layout@ によるリセットに従います)の順にたどり、最も近い ssr の export が ssr = false なら、そのルートはサーバーでレンダリングされません。svelte.config.js で kit.router.type: 'hash' を指定したアプリでは、どのルートもサーバーでレンダリングされません。ssr = dev(dev は $app/environment のもの)は、本番ビルドでの値である false として扱います。ただし、それを export するファイルのモジュールスコープは検査します。SvelteKit がこのオプションを読むためにサーバーで import するからです。それ以外の値で export された ssr は、SSR を再び有効にするものとみなします。同じ階層の universal モジュール(+page.ts/+layout.ts)と server モジュールの両方が export している場合は、SvelteKit と同じく universal 側を採用します。レイアウトは配下のすべてのページがこれに当たる場合に限り、ルートレイアウトは自身が ssr = false を export する場合に限り対象外です。サーバー側のファイル(+page.server.ts、+server.ts、hooks.server.ts)と runes モジュールは引き続き検査します。

なぜ重要か

これらの global は Node に存在しません。モジュールスコープの window 参照はファイルがサーバーで import された瞬間に、load 内なら SSR リクエストのたびにクラッシュします。ReferenceError: window is not defined という、コンパイラが一切警告しない本番 500 です。

修正方法

export function load() {
  const stored = localStorage.getItem('filters'); // ❌ サーバーで ReferenceError

  return {};
}

ブラウザアクセスをクライアント側、onMount の中へ移します。onMount はサーバーでは実行されません。

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

  let stored = $state(null);
  onMount(() => {
    stored = localStorage.getItem('filters'); // ✅ onMount はサーバーでは実行されない
  });
</script>

または明示的にガードします。

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

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

モードによる違い

ありません。このルールが読むのは同じ .svelte / .ts のソースファイルなので、CLI、Vite プラグインのビルド、ライブダッシュボードの静的ベースラインのいずれでも結果は同一で、レンダリング済み HTML の解析で再評価されることもありません。--route で実行範囲を絞ると、このルールは動きません。コンポーネントスコープのルールには、検出を紐づけるルートが無いためです。

無効化

個別に抑制するには、対象行の直前に <!-- svelte-vitals-disable-next-line correctness/server-browser-global --> を置きます。ルールごと無効化するには、次のように設定します。

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