remix-v2-perf-ssr-review について
Remix v2 の TypeScript ルートモジュールを対象とする、パフォーマンスと SSR のレビュー用スキルです。キャッシュ、サーバーとクライアントの境界、水和、プリフェッチ、アセット読み込みを扱い、指摘前に文脈を確認します。
主な機能
- 01キャッシュヘッダーと Cookie 変化の確認
- 02サーバー・クライアント間の境界確認
- 03SSR の水和リスクの特定
- 04プリフェッチとストリーミングデータの点検
- 05画像・フォント・スタイルシート読み込みの確認
活用シーン
- 01ルートのキャッシュ方針のレビュー
- 02サーバーコード漏えいリスクの確認
- 03レンダリングに起因する水和不一致の特定
- 04プリフェッチとアセット読み込み設定の点検
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-perf-ssr-reviewインストール後、Claude Code で/remix-v2-perf-ssr-reviewを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/assets.mdcaching-headers.mdhydration.mdprefetch-streaming.mdserver-client-split.md詳細
Remix v2 のパフォーマンス/SSR コードレビュー
@remix-run/* からインポートする TypeScript のルートモジュールが対象です。標準的なパターンについては、remix-v2-perf-ssr を参照してください。
クイックリファレンス
| 問題の種類 | 参照 |
|---|---|
headers エクスポートの欠落、安全でない public キャッシュ、子ルートによる親のヘッダーの破棄、Vary: Cookie の欠落、Set-Cookie と public の併用 | references/caching-headers.md |
.server.ts なしでのサーバーライブラリのインポート、process.env.SECRET_* の漏えい、.server.ts の代わりに typeof window を使用 | references/server-client-split.md |
レンダリング時の new Date()、キーでの Math.random()、明示的なロケールなしでのロケール形式の出力、useId() の欠落、無差別な suppressHydrationWarning | references/hydration.md |
すべてのリンクへの prefetch="render" の指定、高速なデータへの defer の使用、<Await> を囲む <Suspense> の欠落、副作用のあるルートへのプリフェッチ | references/prefetch-streaming.md |
信頼できないデータでの dangerouslySetInnerHTML、loading="lazy" の欠落、links によるプリロードの欠落、body へのスタイルシートの挿入 | references/assets.md |
レビューチェックリスト
- データを提供するルートは
headersをエクスポートする(no-storeの場合も同様) - 個人向けデータを提供する子ルートは、それぞれ独自の
headersをエクスポートする(そうしないと、親ルートのポリシーが暗黙的に継承されます) - 認証済みまたは Cookie を含むレスポンスに
Cache-Control: publicを設定しない - キャッシュの判断がセッションに依存する場合は
Vary: Cookieを設定する - サーバー専用ライブラリ(
prisma、bcrypt、node:fs、jsonwebtoken)は*.server.tsまたはapp/.server/に配置する - シークレット環境変数(
process.env.STRIPE_SECRET_KEYなど)は、loader/action または.serverモジュール内でのみ読み取る - クライアントに公開する環境変数は
window.ENVにホワイトリスト形式で登録し、生のprocess.envは使わない -
.server.tsの代わりにtypeof window === "undefined"を使用しない(ツリーシェイキングの信頼性が低いため) - JSX のレンダリングパスに
new Date()、Math.random()、Date.now()、crypto.randomUUID()を含めない - ロケール形式での出力(
toLocaleDateString、Intl.DateTimeFormat)には明示的なロケールを渡す - ID を生成するコンポーネントでは
Math.random()やカウンターではなくuseId()を使う -
suppressHydrationWarningは単一の要素に限定し、その理由を説明するコードコメントを付ける -
<Link prefetch="render">は、リストではなくファーストビューに表示される重要なナビゲーションに限定する -
<PrefetchPageLinks>の対象に、副作用(分析、変更)を伴う loader を持つルートを指定しない - すべての
<Await>を<Suspense>で囲み、errorElementを指定する -
defer()は実際に遅いデータ(約 50ms 超)にのみ使い、高速なデータは await する - ファーストビューより下の画像には
loading="lazy"とwidth/heightを指定する - 重要なフォント/CSS は body に挿入せず、
linksエクスポートでプリロードする
有効なパターン(問題として報告しない)
以下は Remix v2 の正しい使い方であり、問題として報告してはいけません。
- 意図的にキャッシュを無効にしている場合の、
headersエクスポートがないルート — 認証済みダッシュボード、アカウントページ、すでにno-storeを返すレイアウトでラップされたルートでは、headersを省略しても問題ありません。headersがなく、キャッシュ可能な公開コンテンツを提供する場合に限り報告してください。 useEffect内のnew Date()— ハイドレーション後にクライアントのみで実行されるため、SSR との不一致は発生しません。エフェクト内のDate.now()、Math.random()、crypto.randomUUID()も同様です。- イベントハンドラー内の
Math.random()/new Date()— ハンドラーはハイドレーション後に実行されます。レンダリング時に値が使用される場合に限り報告してください。 - 明確なコメントが添えられた単一の
<time>(または類似の)要素に対するsuppressHydrationWarning— クライアント側で整形される絶対タイムスタンプのように、値が異なることが既知の場合に使える、限定的で許容される回避策です。大きなサブツリーをラップする親要素に適用されている場合、または説明がない場合に限り報告してください。 - クライアント専用ライブラリの
.client.tsファイル —windowを参照する Stripe.js、マップウィジェット、チャートライブラリは、慣例として*.client.tsに配置します。ファイル拡張子を問題として報告しないでください。 - 追加の文字列を付加した
useId()—`${id}-input`は複数要素コンポーネント向けに文書化されたパターンです。「安定しない ID」として報告しないでください。 - SSR では生の ISO 文字列を描画し、
useEffectで再整形する方法 — ハイドレーションに安全な時間表示の標準的なパターンです。再整形がレンダリング時に行われる場合に限り報告してください。 {}またはno-storeを返すheadersエクスポート — 「キャッシュしない」という明示的な指定は意図的な判断であり、誤用として報告してはいけません。- 標準的なナビゲーションでの
<Link prefetch="intent">— 推奨されるデフォルトです。ローダーに副作用がある場合に限り報告してください。 headersエクスポートを介してドキュメントに転送されるloaderHeaders— データとドキュメントのポリシーを同じ場所にまとめるのは文書化されたパターンであり、重複ではありません。
コンテキスト依存のルール
該当する具体的なコンテキストに限り、以下を適用してください。
| 問題 | 次の場合に限り報告 |
|---|---|
headers エクスポートがない | ルートがキャッシュ可能な公開コンテンツを提供している(認証済みではない、パーソナライズされていない、意図的に no-store にしていない) |
子ルートに headers がない | 祖先が headers をエクスポートしており、そのポリシーが子ルートのキャッシュ可能性より広範である(例:親が公開コンテンツを s-maxage でキャッシュし、子がパーソナライズされたデータを提供する) |
Cache-Control: public | ローダーが実際にセッション/ユーザー状態を読み取る(またはレスポンスに Set-Cookie が含まれる) |
Vary: Cookie がない | ローダーが Cookie に応じてレスポンスの形式を分岐させ(テーマ、ロケール、セッション)、かつキャッシュが public/s-maxage である |
new Date() / Math.random() / Date.now() | 呼び出し箇所がレンダリングパス内にある。useEffect、イベントハンドラー、<ClientOnly>、またはハイドレーション後のコード内ではない |
| ロケールを指定しないロケール整形 | 結果が JSX に描画される(エフェクト/ハンドラー内のみで使用されるのではない) |
<Link prefetch="render"> | リンクがリスト/.map() イテレーター内にある(ファーストビューの重要なナビゲーションではない) |
副作用のあるローダーへの <Link prefetch="intent"> | ローダーに観測可能な副作用(分析データの書き込み、カウンターの増加、ログの出力)があり、かつ Purpose: prefetch ヘッダーで分岐していない |
.server.ts なしでサーバーライブラリをインポートしている | インポート元のファイルがクライアントの依存グラフから到達可能である(ルートモジュール、またはコンポーネントから到達可能な .server ではないユーティリティ) |
process.env.SECRET_* の参照 | 参照箇所がコンポーネント本体内、またはクライアントの依存グラフから到達可能な .server ではないモジュール内にある |
画像に loading="lazy" がない | 画像がファーストビューより下に描画される(<header>、ヒーローセクション内、または <main> コンテンツより上ではない) |
画像に width/height がない | プロジェクトで、ビルド時に寸法を注入する画像プロセッサを使用していない |
指摘を作成する前の必須ゲート
以下を順番に実施してください。これから報告するバッチのすべてのゲートに合格するまで、ユーザー向けの指摘を作成しないでください。
-
場所の証拠 — 合格条件: 各問題に、リポジトリ内のパスと、実際に読んだファイルの行範囲または短い逐語引用を記載します(記憶や差分だけに基づく推測は不可)。具体的なファイルパスがないキャッシュ、ハイドレーション、
.serverに関する主張は報告できません。 -
例外の確認 — 合格条件: 各問題について、有効なパターン(指摘しないこと)の対象外である理由を1行で説明できます。特に、
headersエクスポートの欠落が意図的にキャッシュ不可のルートに該当しないこと、.client.tsが正当なクライアント専用ライブラリではないこと、suppressHydrationWarningがスコープ指定され、コメントで説明されていないことを確認してください。 -
ハイドレーションコンテキストの確認 — 合格条件:
new Date()、Math.random()、Date.now()、crypto.randomUUID()、またはロケールに応じた書式設定を指摘する前に、その呼び出し箇所がコンポーネントのレンダリングパスにあることを確認します。useEffect、useLayoutEffect、イベントハンドラー、setTimeout/requestAnimationFrameに渡されるコールバック内、または<ClientOnly>{() => ...}</ClientOnly>の内部の呼び出しはハイドレーション後であり、指摘してはいけません。 -
親子間のヘッダー継承チェーンの確認 — 合格条件: 「子に
headersがないため、キャッシュ設定が暗黙に継承される」と指摘する前に、一致するルートチェーン内の祖先ルートが実際にheadersをエクスポートしていること(ルートファイルツリーを検索してexport const headersまたはexport function headersを確認)と、継承されるポリシーが子ルートのキャッシュ可能性より広範であることを確認します。祖先がheadersをエクスポートしていない場合、問題は「キャッシュが設定されていない」ことであり、「子が親のキャッシュを暗黙に継承している」ことではありません。 -
サーバー/クライアント境界の確認 — 合格条件: サーバーライブラリのインポートを漏えいとして指摘する前に、インポート元のファイルがクライアントグラフから到達可能であることを確認します。つまり、ルートモジュール、ルートのデフォルトエクスポートから推移的にインポートされる非
.serverユーティリティ、または.client.tsファイルであることが条件です。loader、action、headers、その他の.server.tsモジュール内にあるインポートは漏えいではありません。 -
プロトコル — 合格条件: このレビューについて、review-verification-protocolの報告前検証チェックリストを完了しています。
リファレンスを読み込むタイミング
- ルートの
headersエクスポート、Cache-Control文字列、CDNポリシーをレビューする場合 → references/caching-headers.md .server.ts/.client.tsファイル、prisma/bcrypt/fsのインポート、またはprocess.envへのアクセスをレビューする場合 → references/server-client-split.md- 日付、ID、ロケールに応じて書式設定された値、またはブラウザーのグローバル変数をレンダリングするコンポーネントをレビューする場合 → references/hydration.md
<Link prefetch>、<PrefetchPageLinks>、defer、<Await>、<Suspense>、または<RemixServer abortDelay>をレビューする場合 → references/prefetch-streaming.md<img>、<link>、dangerouslySetInnerHTML、フォント/CSSの読み込み、またはlinksエクスポートをレビューする場合 → references/assets.md
レビューの確認事項
- キャッシュ可能なデータを提供するすべてのルートで、「キャッシュなし」の場合も含め、
Cache-Controlポリシーを宣言していますか? - パーソナライズされたルートで
publicキャッシュを使用しておらず、セッションがレスポンスに影響する場合はVary: Cookieを指定していますか? - サーバー用ライブラリ(
prisma、bcrypt、fs、シークレット環境変数へのアクセス)は、ビルド時に漏洩が拒否される*.server.tsモジュール内にありますか? - 公開環境変数は
process.envから展開するのではなく、ホワイトリストを使ってwindow.ENVに設定していますか? new Date()/Math.random()/ロケール形式の呼び出しは、エフェクト、ハンドラー、または<ClientOnly>内に限定され、レンダー時には使われていませんか?- ID を必要とするコンポーネントでは
useId()を使っていますか? <Link prefetch>のモードはコンテキストに合っていますか(ファーストビュー内ではrenderのみ、ナビゲーションではintent、リストではviewport/intent)?defer()は実際に遅いデータにのみ使用し、<Await>には常に<Suspense>とerrorElementの両方を組み合わせていますか?- 画像にはサイズが指定され、ファーストビューより下では遅延読み込みされ、重要なフォント/CSS は
linksエクスポートを通じてプリロードされていますか? dangerouslySetInnerHTMLは、サニタイズ済みの HTML、または安全にシリアライズされた JS にのみ使用していますか?
指摘を提出する前に
ハードゲート(特にゲート 3 — ハイドレーションコンテキストの確認、およびゲート 5 — サーバー/クライアント境界の確認)を完了してから、review-verification-protocol の報告前チェックを引き続き通過する問題のみを報告してください。指摘の形式:[FILE:LINE] ISSUE_TITLE に、問題のあるコードの引用(原文どおり)と、違反している特定の Remix v2 の仕様に結び付けた1行の根拠を添えてください。
追加ドキュメント
headersエクスポート、CDN キャッシュポリシー、Vary、親/子のマージ、Set-Cookieの相互作用のレビュー → references/caching-headers.md.server.ts/.client.tsの境界、process.envへのアクセス、window.ENVのパターンのレビュー → references/server-client-split.md- レンダー時の
Date/Math.random/ロケールに関する問題、useId、suppressHydrationWarningの適用範囲のレビュー → references/hydration.md <Link prefetch>のモード、<PrefetchPageLinks>のターゲット、defer/<Await>/<Suspense>の構造のレビュー → references/prefetch-streaming.mddangerouslySetInnerHTML、画像のloading/width/height、フォント/CSS のプリロード、スタイルシートの配置のレビュー → references/assets.md- 標準的なパターンと判断ゲート → remix-v2-perf-ssr