Remix V2 Perf Ssr
/install remix-v2-perf-ssr
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/\x3CAwait>), HTTP caching (headers export), prefetching (\x3CLink prefetch> and \x3CPrefetchPageLinks>), 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\x3Ctypeof loader>();
return (
\x3C>
\x3CProductHeader product={product} />
\x3CSuspense fallback={\x3CReviewsSkeleton />}>
\x3CAwait resolve={reviews} errorElement={\x3CReviewsError />}>
{(r) => \x3CReviewList reviews={r} />}
\x3C/Await>
\x3C/Suspense>
\x3C/>
);
}
Streaming with defer and \x3CAwait>
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 \x3CAwait> 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\x3Ctypeof 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 \x3CSkeleton />;
}
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 \x3CClientOnly 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 \x3Cbody>. See references/hydration.md.
Prefetching
Four \x3CLink prefetch> modes: "none" (default), "intent" (hover/focus), "render" (immediate, on render), "viewport" (scrolled into view). Prefetch fires \x3Clink 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 \x3Clink> 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
- Is this data required for the initial paint, meta tags, or SEO (e.g. product title, page title)?
- Pass →
awaitit in the loader, return viajson(). Stop. - Fail → Step 2.
- Pass →
- Is the call genuinely slow (>~50ms, cross-region DB, external API — not in-memory cache)?
- Pass → Pass the unresolved promise through
defer(), wrap in\x3CSuspense>+\x3CAwait errorElement={...}>. Stop. - Fail →
awaitit. Deferring fast data adds streaming overhead and flashes a skeleton for no gain.
- Pass → Pass the unresolved promise through
.server.ts vs runtime typeof window check
- Does the module import
node:*,prisma,bcrypt,jsonwebtoken,fs,path, or readprocess.envat the top level?- Pass → Name the file
*.server.ts(or place underapp/.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.
- Pass → Name the file
- Is the module called only inside
loader/action/headerswith no top-level side effects?- Pass →
.server.tsis still preferred for clarity; tree-shaking may work but is unreliable. Stop. - Fail → Step 3.
- Pass →
- 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).
- Pass →
\x3CLink prefetch> mode selection
- 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.
- Pass →
- 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.
- Pass →
- 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.
- Pass →
- Default:
prefetch="intent"for standard nav (header, sidebar, footer).
Additional Documentation
- Headers and caching: see references/headers-caching.md for
HeadersFunctionsignature,loaderHeaders/parentHeaders/actionHeaders/errorHeaders, SWR patterns, and parent/child merge semantics. - Streaming: see references/streaming.md for
defer(),\x3CAwait>,\x3CSuspense>,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, thewindow.ENVpattern. - Hydration: see references/hydration.md for
useHydrated,\x3CClientOnly>,useId, mismatch grep list. - Prefetch: see references/prefetch.md for
\x3CLink prefetch>modes,\x3CPrefetchPageLinks>, thePurpose: prefetchheader trick. - Preload links: see references/links-preload.md for
linksexport, font/CSS preload, image guidance.
Comparison: When to use which API
| Need | API | Module |
|---|---|---|
| Stream slow secondary data | defer() + \x3CAwait> |
@remix-run/node + @remix-run/react |
| CDN-cache document response | headers export |
route module |
| CDN-cache data response | Cache-Control on json()/Response |
loader return |
| Server-only module | *.server.ts filename |
file convention |
| Browser-only module | *.client.ts filename |
file convention |
| Public env vars in client | window.ENV via root loader |
pattern |
| SSR-safe IDs | useId() |
react |
| Suppress SSR for one component | \x3CClientOnly> |
remix-utils/client-only |
| Branch after hydration | useHydrated() |
remix-utils/use-hydrated |
| Prefetch on hover | \x3CLink prefetch="intent"> |
@remix-run/react |
| Prefetch on render (above-fold) | \x3CLink prefetch="render"> |
@remix-run/react |
| Programmatic prefetch | \x3CPrefetchPageLinks page="/absolute/path"> |
@remix-run/react |
| Preload font/CSS | links export |
route module |
- 确保已安装 OpenClaw(本地或 Docker 部署)
- 在对话框中输入安装命令:
/install remix-v2-perf-ssr - 安装完成后,直接呼叫该 Skill 的名称或使用
/remix-v2-perf-ssr触发 - 根据 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... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 50 次。
如何安装 Remix V2 Perf Ssr?
在 OpenClaw 或 Claude Code 对话框中运行命令「/install remix-v2-perf-ssr」即可一键安装,无需额外配置。
Remix V2 Perf Ssr 是免费的吗?
是的,Remix V2 Perf Ssr 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。
Remix V2 Perf Ssr 支持哪些平台?
Remix V2 Perf Ssr 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。
谁开发了 Remix V2 Perf Ssr?
由 Kevin Anderson(@anderskev)开发并维护,当前版本 v1.0.0。