本文へ移動
cccskills
無料GitHub で公開

ssr-zero-layout-shift

SSR-first Next.js App Router doctrine with zero layout shift. Use when creating a page or data-fetching UI, fixing CLS, layout shift, or hydration mismatches, placing 'use client', Suspense, or skeleton boundaries, awaiting params/searchParams/cookies, adding server-only guards or cache() dedup, or choosing Route Handlers vs Server Actions.

インストール方法を見る

含まれるファイル(4)

  • SKILL.md18.6 KB
  • examples.md17.9 KB
  • nextjs16-patterns-reference.md13.9 KB
  • route-architecture.md14.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

SSR-First Zero Layout Shift

Seven composable patterns that eliminate CLS while maximizing server-rendered content. Every component gets a fixed-dimension Server Component frame; dynamic content streams into sized holes inside those frames.

Core Mental Model

Every piece of UI has two aspects: structure (dimensions, position, borders, labels) and behavior (interactivity, state, fetched data). Structure is always a Server Component. Behavior is always a thin Client Component island nested inside that structure. The browser receives a complete, dimensionally-stable HTML shell instantly. Nothing shifts. Nothing flashes.

The Hierarchy

Server Component defines fixed-dimension container -> Suspense boundary wraps async content -> Skeleton fallback matches exact dimensions -> Client Component island handles only interactive bits -> Lazy loading defers non-critical interactivity until user action -> Redux hydrator (if needed) bridges server data to client store


Pattern 1: Server Component Shell + Client Component Islands

The outer structure is always a Server Component. Only the interactive behavior is a Client Component. The HTML structure is fixed-size, so zero layout shift.

Rules

  • Default to Server Components. Add 'use client' only for hooks, event handlers, or browser APIs.
  • Push 'use client' as far down the tree as possible — wrap the smallest interactive unit, not the container.
  • The Server Component renders the full HTML skeleton (label, container, dimensions). The Client Component only adds behavior.
  • Server Components ship zero JavaScript. Every line inside 'use client' adds to the client bundle.
// Server Component renders structure, Client Component handles the click
import { LikeButton } from './like-button'

export default async function Post({ id }: { id: string }) {
  const post = await getPost(id)
  return (
    <article className="p-6 border rounded-lg">
      <h2>{post.title}</h2>
      <p>{post.body}</p>
      <LikeButton postId={id} initialCount={post.likes} />
    </article>
  )
}

Client Shell -> Server Children (Slot Pattern)

A Client Component can receive Server Components as children. The children render on the server; the Client Component just provides the interactive wrapper.

'use client'
export function CollapsiblePanel({ title, children }: {
  title: string; children: React.ReactNode
}) {
  const [open, setOpen] = useState(true)
  return (
    <section>
      <button onClick={() => setOpen(!open)}>{title}</button>
      {open && children}
    </section>
  )
}

// Server Component composes the pattern
export default async function Sidebar() {
  const stats = await getStats()
  return (
    <CollapsiblePanel title="Stats">
      <StatsDisplay data={stats} />  {/* Server Component passed through */}
    </CollapsiblePanel>
  )
}

Anti-patterns

  • Wrapping an entire form or page in 'use client' when only one input is interactive.
  • Fetching data inside a Client Component when a Server Component parent can pass it as props.
  • Putting the label, container sizing, or layout inside the Client Component.

Pattern 2: Suspense Boundaries as Static Shell with Dynamic Holes

The static shell (layout, labels, known UI) prerenders immediately. Async data fills sized holes via <Suspense>. Everything outside the boundary is in the static shell.

Rules

  • Place <Suspense> as close as possible to the async component — maximize the static shell.
  • Every <Suspense> fallback must be a dimension-matched skeleton (see Pattern 5).
  • Async Server Components inside Suspense stream independently — no waterfalls.
  • Boundary placement heuristic: If two data-fetching components can resolve at different times, they need separate Suspense boundaries. If they always resolve together (same query), they can share one.
export default function Dashboard() {
  return (
    <>
      <PageHeader>
        <h1 className="text-sm font-medium truncate">Dashboard</h1>
      </PageHeader>
      <div className="h-full flex flex-col overflow-hidden">
        <div className="flex-1 grid grid-cols-3 gap-4 p-4">
          <Suspense fallback={<StatCardSkeleton />}><RevenueCard /></Suspense>
          <Suspense fallback={<StatCardSkeleton />}><UsersCard /></Suspense>
          <Suspense fallback={<StatCardSkeleton />}><OrdersCard /></Suspense>
        </div>
      </div>
    </>
  )
}

Each boundary fetches independently and in parallel. This replaces Promise.all when components render different UI.


Pattern 3: Selective Hydration

React 18+ breaks hydration into Suspense-scoped chunks. If a user interacts with a component before hydration completes, React prioritizes hydrating that component first.

Rules

  • Wrap each interactive section in its own <Suspense> boundary to enable independent hydration.
  • Critical interactive elements (search bars, primary CTAs) get their own boundary.
  • One boundary = one streaming unit = one hydration unit. No extra API needed.

Pattern 4: Lazy Loading Client Components (next/dynamic)

Defer loading Client Component code until the user triggers it. The trigger renders in the static shell; the heavy component loads on demand.

Rules

  • Use next/dynamic with ssr: false for components that only matter after user interaction (modals, panels, rich editors).
  • The trigger (button, dropdown trigger) is always server-rendered or in a thin Client Component.
  • Provide a dimension-matched loading component that also says what is loading — a bare pulsing box is a real-loading-states finding.
import dynamic from 'next/dynamic'
import SuspenseLoader from '@/components/loaders/SuspenseLoader'

const RichEditor = dynamic(() => import('./RichEditor'), {
  ssr: false,
  loading: () => (
    <div className="h-64 w-full rounded-md border border-border">
      <SuspenseLoader message="Loading editor…" />
    </div>
  ),
})

Pattern 5: CLS Prevention — Dimension-Matched Skeletons

Every Suspense fallback and loading state must preserve the exact layout dimensions of the real component.

Rules

  • Skeletons must match the real component's height, width, padding, and gap.
  • Use explicit dimension classes (h-32, h-10, w-full) — never rely on content-derived sizing.
  • Skeletons live inside the Suspense boundary, not outside. The outer container is stable regardless.
  • Use the project's <Skeleton> component (Server Component — no 'use client' needed).
  • Mirror the content structure: if the real component has a title and two lines, the skeleton has matching blocks.
  • No spinners for page content. Spinners are acceptable for small inline actions (button loading state).

Validating zero CLS

  1. Same explicit height on the outer container
  2. Same padding and gap classes
  3. Same border/rounding so the visual footprint is identical
  4. No content-dependent sizing — the container never grows based on text length

Pattern 6: Interaction-Triggered Data Fetching

Not all data needs to be fetched on page load. If data is only needed after user action (click, scroll, hover), defer it. The component renders a fixed-dimension shell with a placeholder/hardcoded value. Data fetches only when the user interacts.

Rules

  • The dropdown trigger, form field label, and container dimensions are Server Component output — instant, zero JS.
  • The Client Component fetches data only on first interaction, caches it locally, and replaces the placeholder.
  • Use useTransition to keep the UI responsive during the fetch.
// Server Component — renders the form field structure instantly
export default function AssignAgentField({ currentAgentName }: { currentAgentName: string }) {
  return (
    <div className="flex flex-col gap-1.5">
      <label className="text-sm font-medium">Agent</label>
      <div className="h-10 w-full">
        <AgentDropdown defaultLabel={currentAgentName} />
      </div>
    </div>
  )
}
// Client Component — fetches ONLY on user interaction
'use client'
import { useState, useTransition } from 'react'

export function AgentDropdown({ defaultLabel }: { defaultLabel: string }) {
  const [open, setOpen] = useState(false)
  const [agents, setAgents] = useState<Agent[] | null>(null)
  const [isPending, startTransition] = useTransition()

  const handleOpen = () => {
    setOpen(true)
    if (!agents) {
      startTransition(async () => {
        const data = await fetchAgents()
        setAgents(data)
      })
    }
  }

  return (
    <button onClick={handleOpen} className="h-10 w-full rounded-md border px-3 text-left">
      {defaultLabel}
      {open && (
        <div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover">
          {isPending
            ? <DropdownSkeleton />
            : agents?.map(a => <div key={a.id} className="px-3 py-2">{a.name}</div>)
          }
        </div>
      )}
    </button>
  )
}

When to use each approach

Data needed at...Pattern
Initial page paintFetch in Server Component, pass as props (Pattern 1)
Initial paint, but slowSuspense boundary with skeleton (Pattern 2)
User click/hover/scrollInteraction-triggered fetch (Pattern 6)
Never on this page loadnext/dynamic with ssr: false (Pattern 4)

Pattern 7: Redux Hydration — One-Shot Background Operation

When SSR-fetched data must live in the Redux store for client-side interactions, hydrate it exactly once using a thin Client Component that renders nothing.

'use client'
import { useRef } from 'react'
import { useAppDispatch } from '@/lib/redux/hooks'
import { hydrateAgent } from '@/lib/redux/slices/agentsSlice'

export function AgentHydrator({ agent }: { agent: Agent }) {
  const dispatch = useAppDispatch()
  const hydrated = useRef(false)

  if (!hydrated.current) {
    dispatch(hydrateAgent(agent))
    hydrated.current = true
  }

  return null
}

Rules

  • Why not useEffect: It fires after paint. Children reading from the store would see empty state for one frame, causing a flash. The useRef guard dispatches during the first render pass — before any child reads.
  • Synchronous action only: The Redux action must be a plain synchronous action (not a thunk). The server did the fetching; Redux just receives.
  • Hydrate once: When the layout persists across tab/page navigation, the hydrator doesn't re-mount. Re-hydrating on every navigation risks overwriting in-progress edits.
  • Place in layouts: The hydrator typically lives in a layout that wraps multiple pages sharing the same data.

Data Fetching Architecture

React cache() for Request Deduplication

Wrap data-access functions in React's cache() to deduplicate across the render pass. Layout, page, generateMetadata, and any Server Component can all call the same function — React collapses them into one execution per request.

// lib/data.ts
import { cache } from 'react'
import 'server-only'

export const getAgent = cache(async (id: string) => {
  const supabase = await createClient()
  const { data, error } = await supabase.from('agents').select('*').eq('id', id).single()
  if (error || !data) notFound()
  return data
})

cache() is per-request dedup only (one render pass). Cross-request caching is a separate, rare opt-in — see "Cross-request caching" below; 'use cache' is NOT available here (cacheComponents is off; the directive is a build error).

server-only Import Guard

Add import 'server-only' to any data-access file. If a Client Component accidentally imports it, the build fails immediately — preventing secret leaks and server-only code from entering the client bundle.

Preloading Pattern

When data is expensive and conditionally needed, start the fetch early:

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  preloadAgent(id)
  const canView = await checkPermission(id)
  if (!canView) redirect('/agents')
  return <AgentView id={id} />
}

const preloadAgent = (id: string) => { void getAgent(id) }

Parallel Fetching

// Parallel — both start immediately
const [agent, logs] = await Promise.all([getAgent(id), getAgentLogs(id)])

// Waterfall — logs waits for agent. NEVER do this.
const agent = await getAgent(id)
const logs = await getAgentLogs(id)

Next.js 15/16 App Router Rules (merged from nextjs-app-router-expert and nextjs-ssr-architecture)

Business logic lives in shared services

Never scatter Supabase queries across components and API routes. Extract to a service and consume it from Server Components, API routes, and Server Actions alike:

ScopeLocation
Feature-specificfeatures/[feature-name]/service.ts
Shared/reusablelib/services/[name].ts

Client Components avoid direct Supabase queries where a Server Component parent (via a service) can pass data down. Server clients: @/utils/supabase/server; browser: @/utils/supabase/client.

Async request APIs — always await

params, searchParams, cookies(), and headers() all return Promises in Next.js 15/16:

export default async function Page({ params, searchParams }: {
  params: Promise<{ id: string }>
  searchParams: Promise<Record<string, string>>
}) {
  const { id } = await params
  const { page } = await searchParams
}

export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const cookieStore = await cookies()
}

Types must include the Promise<> wrappers.

Route Handlers vs Server Actions

Use caseUse
Webhooks, third-party integrationsRoute Handlers (/api/...)
Forms / internal mutationsServer Actions

Both go through shared services.

Cross-request caching — dynamic by default

Dynamic rendering is the default. 'use cache' / cacheLife / cacheTag are NOT available (cacheComponents is off; the directive is a build error). Opting in uses the pre-Cache-Components APIs — live exemplar features/education/publishing/queries.ts + actions.ts:

import 'server-only'
import { unstable_cache } from 'next/cache'
import { getScriptSupabaseClient } from '@/utils/supabase/getScriptClient'

// Anon, cookie-free client ONLY — a per-user read inside unstable_cache serves one user's data to everyone.
export const listPublishedDocs = unstable_cache(
  async () => fetchPublishedRows(getScriptSupabaseClient()),
  ['docs:list'],                       // keyParts must carry every argument, or all args collapse onto one entry
  { tags: ['docs'], revalidate: 3600 },
)
// Invalidate from a Server Action: updateTag('docs') (Route Handler: revalidateTag).
  • Route-level ISR: export const revalidate = <seconds> on a cookie-free page (app/(core)/education/learn/page.tsx).
  • User-specific data is never cached across requests — it renders dynamically inside <Suspense>.
  • Reference: node_modules/next/dist/docs/01-app/02-guides/caching-without-cache-components.md.

"use client" audit

"use client" is justified ONLY by hooks, event handlers, browser APIs, or client-only third-party libs. A pure display component carrying the directive is a violation — remove it. When reviewing, check each in order: hooks? events? browser APIs? None → strip the directive.


Loading State Hierarchy

layout.tsx        → Never shows loading (persists across navigation)
loading.tsx       → Wraps page.tsx in Suspense (route transition skeleton)
<Suspense>        → Wraps individual async components (granular streaming)
dynamic(ssr:false)→ Placeholder until client JS loads (interaction-triggered)

loading.tsx is automatic Suspense at the page level. Prefer granular <Suspense> boundaries inside pages for fine-grained streaming. Use loading.tsx as a coarse fallback for full-page transitions.


Decision Tree

Building a new component?
│
├── Does it fetch data?
│   ├── Yes, needed at initial paint → async Server Component in <Suspense> (Pattern 2)
│   ├── Yes, but only after interaction → Interaction-triggered fetch (Pattern 6)
│   └── No → Render directly in the static shell
│
├── Does it need interactivity?
│   ├── Yes → Can the interactive part be isolated?
│   │   ├── Yes → Server parent + thin Client child (Pattern 1)
│   │   └── No → Minimal 'use client' component
│   ├── Is it critical (above fold)? → Own Suspense boundary (Pattern 3)
│   └── Is it non-critical? → next/dynamic ssr:false (Pattern 4)
│
├── Does it display user-specific data (cookies, session)?
│   ├── Yes → Dynamic Server Component inside Suspense (never cached across requests)
│   └── No → Dynamic by default; hot + cookie-free → unstable_cache with tags/revalidate
│
└── Does the fetched data need to be in Redux?
    ├── Yes → Add Hydrator component in layout (Pattern 7)
    └── No → Pass as props

Checklist

Before shipping any page or component:

  • Outer container has explicit dimensions (not content-derived)
  • 'use client' only on the smallest interactive unit
  • Every <Suspense> fallback matches the real component's bounding box
  • Async data fetches happen in Server Components, not Client Components
  • Non-critical interactive components use next/dynamic with ssr: false
  • No data waterfalls — parallel Suspense boundaries or Promise.all
  • Skeleton components are Server Components (no 'use client' directive)
  • Data-access functions wrapped in cache() with import 'server-only'
  • Redux hydration (if needed) uses useRef guard, not useEffect
  • Non-critical data defers to interaction-triggered fetching

Additional Resources

  • For complete page examples (cached + dynamic sections), see examples.md
  • For route-level architecture (layouts, nested routes, metadata, error boundaries), see route-architecture.md
  • For detailed Next.js 16 templates (services, API routes, signatures), see nextjs16-patterns-reference.md
  • For splitting heavy client code out of the bundle, see the code-splitting skill; for the full new-route workflow, see new-route-scaffold

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Admin debug system wiring for the floating AdminIndicator's live debug data and Copy Full Context. Use when adding debug visibility to a route or feature, wiring useDebugContext or the debug panel, capturing console errors, or enabling the copy-context workflow for a page.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout; or writing a Copy-for-AI payload. NOT for markdown content actions (use rich-document-actions).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Disclosing a surface's existing fixed AI jobs in the shell's top Agents menu. Use when a page, panel, overlay, or window already runs a mandate behind a button, assist, automatic action, or mode; when the agent-disclosure guard names a file; or during a surface check.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The watch-fix-rerun method for making an existing platform agent or automated process efficient and correct: baseline its ledger, run one unit yourself, fix the class behind every wasted call, rerun, record. Use when asked to improve, optimize, watch, or 'make efficient' an agent, a sandbox session, a sync job, or any recurring automated process.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Redux state for agent execution and firing agent shortcuts. Use when editing features/agents/redux/, building agent UI, creating a conversation, touching assembleRequest or NDJSON stream state, wiring agent-state selectors, adding a per-conversation capability, or triggering a shortcut from a button, menu, or code (useShortcutTrigger, launchAgentExecution).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The provision for a call site: the exhaustive menu of values that place in the code can realistically produce. Use before creating or fixing a mandate or agent that reasons about more than its own input, when deciding whether an agent can answer at all with what it is sent, or when its output 'looks right' but nobody checked it could be. NOT for building the agent (use create-agent).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

armanisadeghi のスキルをすべて見る

このスキルの問題を報告する