Skip to content
Development
Skill

/remix-v2-perf-ssr-review

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

From plugin
beagle
82139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-perf-ssr-review --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/remix-v2-perf-ssr-review

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

remix-v2-perf-ssr-review.SKILL.md
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 [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 fo

Read more
Ships withbeagle

Image: NASA, Public Domain. Source Beagle is an Agent Skills marketplace: framework-aware code review, documentation, testing, architectural analysis, and git workflows for any compatible coding agent.

Get the whole plugin

Other skills on beagle.