Claude
Skills
Sign in
Back

remix-v2-perf-ssr-review

Included with Lifetime
$97 forever

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.

Image & Video

What this skill does


# Remix v2 Performance / SSR Code Review

Targets TypeScript route modules importing from `@remix-run/*`. See [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 `<Suspense>` around `<Await>`, 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
- [ ] `<Link prefetch="render">` is reserved for above-the-fold critical nav, not lists
- [ ] `<PrefetchPageLinks>` does not target routes whose loaders have side effects (analytics, mutations)
- [ ] Every `<Await>` is wrapped in `<Suspense>` 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 `<time>` (or similar) element with a clear comment** — accepted narrow escape for known-divergent values like absolute timestamps formatted client-side. Flag only when applied at a parent that wraps a large subtree or with no explanation.
- **`.client.ts` files for client-only libraries** — Stripe.js, map widgets, chart libs that read `window` belong in `*.client.ts` by convention; do not flag the file extension.
- **`useId()` with extra characters appended** — `` `${id}-input` `` is the documented pattern for multi-element components; do not flag as "non-stable id."
- **Raw ISO string rendered in SSR + reformatted in `useEffect`** — the canonical hydration-safe time pattern; flag only if the reformat happens in render.
- **`headers` export returning `{}` or `no-store`** — explicit "do not cache" is a deliberate decision and should not be flagged as misuse.
- **`<Link prefetch="intent">` on standard nav** — the recommended default; flag only when the loader has side effects.
- **`loaderHeaders` forwarded to the document via `headers` export** — co-locating data and document policy is the documented pattern, not duplication.

## Context-Sensitive Rules

Apply these only when the specific context applies:

| Issue | Flag ONLY IF |
|-------|--------------|
| Missing `headers` export | Route serves cacheable public content (not auth'd, not personalized, not intentionally `no-store`) |
| Child route missing `headers` | An ancestor exports `headers` AND its policy is broader than the child's cacheability (e.g., parent caches public + s-maxage, child serves personalized data) |
| `Cache-Control: public` | Loader actually reads session / user state (or response carries `Set-Cookie`) |
| `Vary: Cookie` missing | Loader branches response shape on a cookie (theme, locale, session) AND the cache is `public`/`s-maxage` |
| `new Date()` / `Math.random()` / `Date.now()` | Call site is in render path — NOT in `useEffect`, event handler, `<ClientOnly>`, or post-hydration code |
| Locale formatting without locale | Result is rendered into JSX (not used only inside an effect / handler) |
| `<Link prefetch="render">` | Link is inside a list / `.map()` iterator (not above-the-fold critical nav) |
| `<Link prefetch="intent">` to side-effect loader | Loader has observable side effects (analytics write, counter increment, log emit) AND doesn't branch on the `Purpose: prefetch` header |
| Server lib import without `.server.ts` | Importing file is reachable from the client graph (route module, non-`.server` util reached from a component) |
| `process.env.SECRET_*` reference | Reference is in a component body or in a non-`.server` module reached from the client graph |
| Missing `loading="lazy"` on image | Image is rendered below the fold (not in `<header>`, hero section, or above any `<main>` content) |
| Missing `width`/`height` on image | Project does NOT use a build-time image processor that injects dimensions |

## Hard gates (before writing findings)

Run these in order. **Do not draft user-facing findings until every gate passes** for the batch you are about to report.

1. **Location evidence** — **Pass:** Each issue lists the repo path and either a line range or a short verbatim quote from the file you read (not memory or diff-only guesswork). Cache, hydration, and `.server` claims without a concrete file path are not reportable.

2. **Exemption check** — **Pass:** For each issue, you can state in one line why it is *not* covered by [Valid Patterns (Do NOT Flag)](#valid-patterns-do-not-flag). In particular: confirm a missing `headers` export is not on an intentionally-uncacheable route, confirm `.client.ts` is not a legitimate client-only library, confirm `suppressHydrationWarning` is not scoped + commented.

3. **Hydration-context check** — **Pass:** Before flagging `new Date()`, `Math.random()`, `Date.now()`, `crypto.randomUUID()`, or locale formatting, confirm the call site is in the **render path** of a component. Calls inside `useEffect`, `useLayoutEffect`, event handlers, callbacks passed to `setTimeout`/`requestAnimationFrame`, or inside `<ClientOnly>{() => ...}</ClientOnly>` are post-hydration and must not be flagged.

4. **Parent/child headers chain check** — **Pass:** Before flagging "missing `headers` on a child" as silent cache inheritance, confirm an ancestor route in the

Related in Image & Video