Skip to content
Development
Skill

/remix-v2-perf-ssr

Remix v2 performance, streaming, caching, and server/client boundaries. Use when configuring HTTP caching, server-only modules, hydration safety, or prefetch. Triggers on headers export, Cache-Control, PrefetchPageLinks, Link prefetch, .server.ts, .client.ts, useHydrated,

From plugin
beagle
82139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-perf-ssr --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

Context preview

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

Remix v2 performance, streaming, caching, and server/client boundaries. Use when configuring HTTP caching, server-only modules, hydration safety, or prefetch. Triggers on headers export, Cache-Control, PrefetchPageLinks, Link prefetch, .server.ts, .client.ts, useHydrated,

SKILL.md

remix-v2-perf-ssr.SKILL.md
name: remix-v2-perf-ssr
description: Remix v2 performance, streaming, caching, and server/client boundaries. Use when configuring HTTP caching, server-only modules, hydration safety, or prefetch. Triggers on headers export, Cache-Control, PrefetchPageLinks, Link prefetch, .server.ts, .client.ts, useHydrated, ClientOnly, window.ENV, links preload, useId.

Remix v2 Performance, Streaming, Caching, Server/Client Split

Remix v2 has no built-in image optimizer and no opaque framework cache — it pushes everything to the standard HTTP layer. The performance surface is four pillars: streaming (`defer`/`<Await>`), HTTP caching (`headers` export), prefetching (`<Link prefetch>` and `<PrefetchPageLinks>`), and a hard server/client split (`.server.*` / `.client.*` file conventions).

Quick Reference

**`headers` export with SWR (forward loader headers to the document)**:

import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await cms.getPost(params.slug);
  return json(post, {
    headers: {
      "Cache-Control":
        "public, max-age=60, s-maxage=3600, stale-while-revalidate=86400",
    },
  });
}

export const headers: HeadersFunction = ({ loaderHeaders }) => ({
  "Cache-Control": loaderHeaders.get("Cache-Control") ?? "no-store",
});

**`.server.ts` for server-only modules** — build fails loud if the file leaks into the client graph:

// app/lib/db.server.ts — never bundled into the client
import { PrismaClient } from "@prisma/client";
export const db = new PrismaClient();

**`defer()` for slow secondary data**:

import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export async function loader({ params }: LoaderFunctionArgs) {
  const product = await db.getProduct(params.id);   // critical, awaited
  const reviews = db.getReviews(params.id);          // slow, not awaited
  return defer({ product, reviews });
}

export default function Product() {
  const { product, reviews } = useLoaderData<typeof loader>();
  return (
    <>
      <ProductHeader product={product} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Await resolve={reviews} errorElement={<ReviewsError />}>
          {(r) => <ReviewList reviews={r} />}
        </Await>
      </Suspense>
    </>
  );
}

Streaming with `defer` and `<Await>`

Every promise passed to `defer` must be created **before** any `await` in the loader, otherwise the loader still blocks on the slow call and streaming gains nothing. Always pair `<Await>` with `errorElement` — without it, a rejected deferred promise bubbles to the route's `ErrorBoundary` and tears down the whole route, defeating the streaming benefit.

See [references/streaming.md](references/streaming.md) for full coverage.

HTTP Caching via `headers`

`max-age` controls browser cache; `s-maxage` controls shared/CDN cache and overrides `max-age` at the CDN; `stale-while-revalidate` lets the CDN serve stale content while it refreshes in the background. Two cache scopes exist per route: the **document** response (controlled by the `headers` export) and the **data** request (the `?_data=` JSON request fired on client-side navigation — controlled by the loader's response headers). They can — and often should — carry different policies.

**Parent/child merge is "deepest route wins"**: only the deepest matched route's `headers` runs by default. If a child route has no `headers` export, Remix walks up to the nearest parent that does. The safest rule: define `headers` only on leaf routes, never on layouts that wrap personalized children. Otherwise an aggressive parent policy silently caches per-user HTML at the CDN.

When merging in a child, pick the **smaller** `max-age` — never widen a parent's caching policy from a child:

export const headers: HeadersFunction = ({ loaderHeaders, parentHeaders }) => {
  const loader = parseCacheControl(loaderHeaders.get("Cache-Control"));
  const parent = parseCacheControl(parentHeaders.get("Cache-Control"));
  const maxAge = Math.min(loader["max-age"] ?? 0, parent["max-age"] ?? 0);
  return { "Cache-Control": `private, max-age=${maxAge}` };
};

See [references/headers-caching.md](references/headers-caching.md).

Server/Client Split

The compiler strips `loader`, `action`, and `headers` exports from client bundles along with the dependencies used **inside them** — but only if those dependencies have no module side effects. A top-level `new PrismaClient()`, a `console.log`, an `initializeApp` call all defeat tree-shaking. Rule: any module that imports `node:fs`, `prisma`, `bcrypt`, `jsonwebtoken`, or reads `process.env` should be named `*.server.ts` (or live under `app/.server/` — directory form requires the Remix Vite plugin; Classic Compiler supports only the filename suffix). Build fails loud if it reaches the client graph — silent leaks are eliminated.

Public env vars reach the browser via a root-loader `window.ENV` pattern. Never return raw `process.env` from a loader. See [references/server-client-split.md](references/server-client-split.md).

`clientLoader` and `clientAction`

v2 added optional `clientLoader` / `clientAction` exports that run in the browser alongside (or instead of) the server `loader`/`action`. By default `clientLoader` does **NOT** run on initial hydration — the server `loader` SSRs the page, and `clientLoader` only fires on subsequent client navigations. Opt in to first-render execution with `clientLoader.hydrate = true` and export a `HydrateFallback` component to render while it executes:

import type { ClientLoaderFunctionArgs } from "@remix-run/react";

export async function loader() {
  return json({ /* SSR data */ });
}

export async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) {
  const cached = clientCache.get();
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.