--- name: remix-v2-perf-ssr-review description: Reviews Remix v2 code for caching header misuse, missing server/client split, hydration mismatches (Date, Math.random, locale), prefetch hygiene, and asset bottlenecks. Use when reviewing routes that export headers, use .server.ts/.client.ts, or render dates/IDs in a Remix v2 codebase. --- # Remix v2 Performance / SSR Code Review Targets TypeScript route modules importing from `@remix-run/*`. See [beagle-react:remix-v2-perf-ssr](../remix-v2-perf-ssr/SKILL.md) for canonical patterns. ## Quick Reference | Issue Type | Reference | |------------|-----------| | Missing `headers` export, unsafe `public` cache, child-drops-parent headers, missing `Vary: Cookie`, `Set-Cookie` + `public` | [references/caching-headers.md](references/caching-headers.md) | | Server libs imported without `.server.ts`, `process.env.SECRET_*` leaks, `typeof window` substituted for `.server.ts` | [references/server-client-split.md](references/server-client-split.md) | | `new Date()` in render, `Math.random()` in keys, locale formatting without explicit locale, missing `useId()`, blanket `suppressHydrationWarning` | [references/hydration.md](references/hydration.md) | | `prefetch="render"` on every link, `defer` for fast data, missing `` around ``, prefetch to side-effect routes | [references/prefetch-streaming.md](references/prefetch-streaming.md) | | `dangerouslySetInnerHTML` with untrusted data, missing `loading="lazy"`, missing `links` preload, stylesheet injected in body | [references/assets.md](references/assets.md) | ## Review Checklist - [ ] Routes serving data export `headers` (even if the answer is `no-store`) - [ ] Child routes serving personalized data export their own `headers` (otherwise they silently inherit the parent's policy) - [ ] `Cache-Control: public` is never set on auth'd or cookie-bearing responses - [ ] `Vary: Cookie` is set when cache decision depends on session - [ ] Server-only libs (`prisma`, `bcrypt`, `node:fs`, `jsonwebtoken`) live in `*.server.ts` or `app/.server/` - [ ] Secret env (`process.env.STRIPE_SECRET_KEY`, etc.) is read only inside loaders/actions or `.server` modules - [ ] Client-exposed env is whitelisted into `window.ENV`, never raw `process.env` - [ ] `typeof window === "undefined"` is not used as a substitute for `.server.ts` (treeshaking is unreliable) - [ ] No `new Date()`, `Math.random()`, `Date.now()`, `crypto.randomUUID()` in JSX render path - [ ] Locale formatting (`toLocaleDateString`, `Intl.DateTimeFormat`) passes an explicit locale - [ ] Components generating IDs use `useId()`, not `Math.random()` or counters - [ ] `suppressHydrationWarning` is scoped to a single element with a code comment explaining why - [ ] `` is reserved for above-the-fold critical nav, not lists - [ ] `` does not target routes whose loaders have side effects (analytics, mutations) - [ ] Every `` is wrapped in `` and has an `errorElement` - [ ] `defer()` is used only for genuinely slow data (>~50ms); fast data is awaited - [ ] Below-the-fold images use `loading="lazy"` and have `width`/`height` - [ ] Critical fonts/CSS are preloaded via the `links` export, not injected in body ## Valid Patterns (Do NOT Flag) These are correct Remix v2 usage and must not be reported as issues: - **Route without `headers` export when caching is intentionally off** — auth'd dashboards, account pages, and routes wrapped in a layout that already returns `no-store` may legitimately omit `headers`. Flag only if the route serves cacheable public content with no `headers`. - **`new Date()` inside `useEffect`** — runs after hydration on the client only; no SSR mismatch possible. Same for `Date.now()`, `Math.random()`, `crypto.randomUUID()` inside effects. - **`Math.random()` / `new Date()` inside event handlers** — handlers run after hydration. Only flag when the value is used during render. - **`suppressHydrationWarning` on a single `