본문으로 건너뛰기
cccskills
무료GitHub에 공개

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, ClientOnly, window.ENV, links preload, useId.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(7)

  • SKILL.md12.9 KB
  • references/headers-caching.md9.2 KB
  • references/hydration.md7.2 KB
  • references/links-preload.md5.3 KB
  • references/prefetch.md7.6 KB
  • references/server-client-split.md7.7 KB
  • references/streaming.md7.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

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 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.

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.

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();
  if (cached) return cached;
  const fresh = await serverLoader<typeof loader>(); // round-trip to server loader
  clientCache.set(fresh);
  return fresh;
}
clientLoader.hydrate = true; // opt in to running on initial hydration

export function HydrateFallback() {
  return <Skeleton />;
}

Use clientLoader for: client-side caching of server payloads, reading from IndexedDB / localStorage after hydration, fully client-only routes (skip loader entirely). Do NOT re-fetch the same server payload SSR'd by the route's loader — that's a wasted round-trip; either call serverLoader() and cache, or only run on transitions (leave hydrate false).

Hydration Safety

useHydrated() returns false during SSR and on the very first client render, then flips to true on the next render — that two-pass behavior is what keeps HTML matched. For components that should never SSR (maps, charts that read window), wrap in <ClientOnly fallback={...}>. For SSR-safe IDs use React's useId(), never Math.random() or crypto.randomUUID() in render.

The hydration-mismatch grep list: new Date(, Math.random(, crypto.randomUUID(, Date.now(, window., document., localStorage, sessionStorage, navigator., Intl.DateTimeFormat() without an explicit locale, Intl.NumberFormat, .toLocaleDateString, .toLocaleTimeString, .toLocaleString, process.env. in component bodies, typeof window ternaries that produce different JSX, third-party scripts that mutate the DOM, browser extensions injecting nodes into <body>. See references/hydration.md.

Prefetching

Four <Link prefetch> modes: "none" (default), "intent" (hover/focus), "render" (immediate, on render), "viewport" (scrolled into view). Prefetch fires <link rel="prefetch"> tags as siblings of the anchor — use :last-of-type in CSS, not :last-child, because the prefetch tags briefly become last child.

A subtle gotcha: hover-prefetch with no Cache-Control on the loader doubles the request count because the browser doesn't cache the prefetch response. Detect the Purpose: prefetch header in the loader and return Cache-Control: private, max-age=10. See references/prefetch.md.

Asset Preloading via links

The links export injects <link> tags into the document head — preload critical fonts and CSS, prefetch likely-next-page assets. Remix has no built-in image optimizer; size images at build time (sharp, unpic, remix-image) and always set width/height. See references/links-preload.md.

Gates (decision sequencing)

Answer in order. Pass means the condition is true; pick the API on the same line and stop.

defer() vs awaiting in the loader

  1. Is this data required for the initial paint, meta tags, or SEO (e.g. product title, page title)?
    • Pass → await it in the loader, return via json(). Stop.
    • Fail → Step 2.
  2. Is the call genuinely slow (>~50ms, cross-region DB, external API — not in-memory cache)?
    • Pass → Pass the unresolved promise through defer(), wrap in <Suspense> + <Await errorElement={...}>. Stop.
    • Fail → await it. Deferring fast data adds streaming overhead and flashes a skeleton for no gain.

.server.ts vs runtime typeof window check

  1. Does the module import node:*, prisma, bcrypt, jsonwebtoken, fs, path, or read process.env at the top level?
    • Pass → Name the file *.server.ts (or place under app/.server/ — directory form requires the Remix Vite plugin; Classic Compiler supports only the filename suffix). Build fails loud if leaked to client. Stop.
    • Fail → Step 2.
  2. Is the module called only inside loader/action/headers with no top-level side effects?
    • Pass → .server.ts is still preferred for clarity; tree-shaking may work but is unreliable. Stop.
    • Fail → Step 3.
  3. Is the code legitimately isomorphic but needs to branch on environment (logger, feature flag)?
    • Pass → typeof window === "undefined" is acceptable inside a function body — never at module top level (the dead branch can pull server deps into the client graph).

<Link prefetch> mode selection

  1. Is the link sensitive, expensive, or has loader side effects (logout, analytics-instrumented page view, mutation-triggering loader)?
    • Pass → prefetch="none". Stop.
    • Fail → Step 2.
  2. Is this an above-the-fold critical nav link likely to be the next click?
    • Pass → prefetch="render". Loader/JS/CSS prefetched immediately. Stop.
    • Fail → Step 3.
  3. Is the link in a long list (table row, search results, feed)?
    • Pass → prefetch="viewport" (fires when scrolled in) or "intent" (fires on hover). Never "render" on long lists. Stop.
    • Fail → Step 4.
  4. Default: prefetch="intent" for standard nav (header, sidebar, footer).

Additional Documentation

  • Headers and caching: see references/headers-caching.md for HeadersFunction signature, loaderHeaders/parentHeaders/actionHeaders/errorHeaders, SWR patterns, and parent/child merge semantics.
  • Streaming: see references/streaming.md for defer(), <Await>, <Suspense>, abortDelay, error handling, CSP interactions.
  • Server/client split: see references/server-client-split.md for .server.* / .client.* (directory form requires the Remix Vite plugin; Classic Compiler supports only the filename suffix), env var handling, the window.ENV pattern.
  • Hydration: see references/hydration.md for useHydrated, <ClientOnly>, useId, mismatch grep list.
  • Prefetch: see references/prefetch.md for <Link prefetch> modes, <PrefetchPageLinks>, the Purpose: prefetch header trick.
  • Preload links: see references/links-preload.md for links export, font/CSS preload, image guidance.

Comparison: When to use which API

NeedAPIModule
Stream slow secondary datadefer() + <Await>@remix-run/node + @remix-run/react
CDN-cache document responseheaders exportroute module
CDN-cache data responseCache-Control on json()/Responseloader return
Server-only module*.server.ts filenamefile convention
Browser-only module*.client.ts filenamefile convention
Public env vars in clientwindow.ENV via root loaderpattern
SSR-safe IDsuseId()react
Suppress SSR for one component<ClientOnly>remix-utils/client-only
Branch after hydrationuseHydrated()remix-utils/use-hydrated
Prefetch on hover<Link prefetch="intent">@remix-run/react
Prefetch on render (above-fold)<Link prefetch="render">@remix-run/react
Programmatic prefetch<PrefetchPageLinks page="/absolute/path">@remix-run/react
Preload font/CSSlinks exportroute module

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Use when you need to mine a conversation, session transcript, or design discussion for architectural decisions before writing ADRs. Identifies problem-solution pairs, trade-off debates, technology choices, and explicit "[ADR]" tags. Triggers on "what decisions did we make", "extract decisions from this chat", "find the choices in our discussion", or "summarize architectural decisions". Also useful after long planning sessions to capture decisions that were made implicitly. Does NOT write ADR documents — use adr-writing or write-adr for that.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

Use when writing or formatting an ADR document using the MADR template, applying Definition of Done (E.C.A.D.R.) criteria, or verifying ADR completeness. Triggers on "write the ADR", "format as MADR", "check ADR quality", "mark gaps in ADR". Also triggers when a decision has been extracted and needs to become a document. Does NOT extract decisions from conversations (use adr-decision-extraction) or orchestrate the full extract-confirm-write workflow (use write-adr).

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

Use when auditing an agent codebase against the 12-Factor Agents methodology, reviewing LLM-powered system architecture, or assessing agentic app compliance. Triggers on "analyze agent architecture", "12-factor audit", "how compliant is this agent", or "evaluate this LLM app". Also applies when comparing frameworks or planning agent improvements. Not for quick checklists — this performs deep per-factor codebase analysis with file-level evidence.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

Vercel AI Elements for workflow UI components. Use when building chat interfaces, displaying tool execution, showing reasoning/thinking, or creating job queues. Triggers on ai-elements, Queue, Confirmation, Tool, Reasoning, Shimmer, Loader, Message, Conversation, PromptInput.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

Reviews App Intents code for intent structure, entities, shortcuts, and parameters. Use when reviewing code with import AppIntents, @AppIntent, AppEntity, AppShortcutsProvider, or @Parameter.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

Use when the user wants a cited, structured read of local documents and project knowledge. Triggers on: "analyze these docs", "scan my project for context", "read the docs folder", "summarize what's in .beagle/concepts/", "extract context from docs/", "what's in this folder", "go read everything in X and tell me what's there". Also invoked programmatically by other beagle skills (prfaq-beagle Ignition, brainstorm-beagle reference points, strategy-interview context grounding) via the companion contract. Does NOT trigger on codebase lookups ("find this function", "search the repo"), web research (use web-research), LLM-as-judge evaluation (use llm-judge), or document editing (use humanize-beagle). Produces a written scan plan, parallel-subagent findings, and a cited synthesis report on disk — never inline prose, never unsourced claims.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

existential-birds/beagle842026년 9월 16일 업데이트

existential-birds의 스킬 모두 보기

이 스킬의 문제 신고