スキル一覧に戻る
violabg

nextjs-performance-architecture

by violabg

0🍴 0📅 2026年1月20日
GitHubで見るManusで実行

SKILL.md


name: nextjs-performance-architecture description: A comprehensive guide to Next.js performance patterns, combining Data Fetching Colocation, the Donut Pattern, and the 'use cache' directive for optimal application architecture.

This document aggregates three core architectural patterns for modern Next.js 16+ development: Data Fetching Colocation, The Donut Pattern, and Cache Components with use cache. These patterns are designed to improve performance, maintainability, and code composition.

Prerequisites: Next.js 16+ with cacheComponents: true enabled in next.config.ts.


Quick Decision Guide

Use this flowchart to choose the right pattern:

┌─────────────────────────────────────────────────────────────────┐
│                    Component Rendering Decision                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                ┌─────────────────────────────┐
                │ Does it need user state,    │
                │ event handlers, or hooks?   │
                └─────────────────────────────┘
                     │              │
                   Yes              No
                     │              │
                     ▼              ▼
            ┌────────────┐   ┌─────────────────────┐
            │ "use       │   │ Keep as Server      │
            │ client"    │   │ Component           │
            └────────────┘   └─────────────────────┘
                                    │
                                    ▼
                      ┌─────────────────────────────┐
                      │ Does it fetch data or do    │
                      │ expensive computation?      │
                      └─────────────────────────────┘
                           │              │
                         Yes              No
                           │              │
                           ▼              ▼
            ┌──────────────────────┐   Static in shell
            │ Is data user/request │   (automatic)
            │ specific?            │
            └──────────────────────┘
                  │         │
                Yes         No
                  │         │
                  ▼         ▼
         ┌─────────────┐  ┌─────────────┐
         │ Wrap in     │  │ Add         │
         │ <Suspense>  │  │ "use cache" │
         └─────────────┘  └─────────────┘

1. Data Fetching Colocation

When to Use

  • Data is passed through multiple layers of components (prop drilling)
  • Root layout/page is blocked by a large initial data fetch
  • Components are not reusable because they depend on props from a specific parent

Implementation

Move async fetch calls directly into the Server Component that consumes the data:

// ❌ Before: Prop drilling blocks parallelism
export default async function Page() {
  const data = await getData();
  return <Child data={data} />;
}

// ✅ After: Collocated fetching enables parallel loading
export default async function Child() {
  const data = await getData();
  return <div>{data.title}</div>;
}

Resolving Promises with use()

Pass promises directly to Client Components and unwrap with React.use():

// Server Component
export default function Page() {
  const userPromise = getUser(); // Don't await!
  return (
    <Suspense fallback={<UserSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// Client Component
"use client";
import { use } from "react";

export function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // Suspends until resolved
  return <div>{user.name}</div>;
}

❌ Anti-Patterns

  • Fetching all data at page level and threading through props
  • Using useEffect + useState for data that could be fetched server-side
  • Duplicating fetch logic across components instead of colocating

2. The Donut Pattern

When to Use

  • Adding interactivity to a page section while keeping nested content server-rendered
  • Avoiding "use client" on a large component tree for a small interactive element
  • Preserving async capability in deeply nested Server Components

Implementation

  1. Isolate Interactive Logic → Extract into a Client Component
  2. Create the "Hole" → Accept children as a prop
  3. Compose on Server → Pass Server Components as children
// AnimatedContainer.tsx (Client Component - the "donut")
"use client";
import { motion } from "framer-motion";

export function AnimatedContainer({ children }: { children: React.ReactNode }) {
  return (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
      {children}
    </motion.div>
  );
}

// Page.tsx (Server Component)
import { AnimatedContainer } from "./AnimatedContainer";
import { ProductList } from "./ProductList"; // Server Component

export default function Page() {
  return (
    <AnimatedContainer>
      {/* ProductList runs on server, not included in client bundle */}
      <ProductList />
    </AnimatedContainer>
  );
}

Benefits

  • Reduced Bundle Size: Server Component code stays on server
  • Async Support: Inner components can still be async and fetch data
  • Animation/Interactivity: Outer wrapper handles client-side concerns

❌ Anti-Patterns

  • Marking entire page as "use client" to add one click handler
  • Putting data fetching in Client Components when it could be server-side
  • Nesting Client Components unnecessarily deep

3. Cache Components with use cache

Cache Components let you mix static, cached, and dynamic content in a single route—the speed of static sites with the flexibility of dynamic rendering.

Setup

Enable in next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

How It Works

At build time, Next.js renders your route. Components that don't access network resources or request data are automatically static. For others, you choose:

ScenarioSolution
Needs request data (cookies, headers, user-specific)Wrap in <Suspense>
Expensive but static/sharedAdd "use cache"
Mix of bothCombine patterns (Donut + Cache)

Basic Usage

// File-level caching (all exports cached)
"use cache";

export async function getProducts() {
  return await db.product.findMany();
}

export async function getCategories() {
  return await db.category.findMany();
}
// Function-level caching
export async function getFeaturedSkills() {
  "use cache";
  return await db.skill.findMany({ where: { featured: true } });
}

Cache Lifetime with cacheLife()

Control how long cached content lives using preset profiles:

ProfileUse CaseStaleRevalidateExpire
"seconds"Real-time data (stock prices)0s1s60s
"minutes"Frequently updated (feeds)5min1min1h
"hours"Moderately static (blog posts)5min1h1d
"days"Rarely changing (product catalog)5min1d1w
"weeks"Very stable (landing pages)5min1w1mo
"max"Immutable (versioned assets)5min1yindefinite
import { cacheLife } from "next/cache";

export async function ProductCatalog() {
  "use cache";
  cacheLife("days"); // Cache for ~1 day

  const products = await db.product.findMany();
  return <ProductGrid products={products} />;
}

Conditional Cache Lifetimes

import { cacheLife, cacheTag } from "next/cache";

async function getPostContent(slug: string) {
  "use cache";
  cacheTag(`post-${slug}`);

  const post = await fetchPost(slug);

  if (!post) {
    cacheLife("minutes"); // Missing content, check again soon
    return null;
  }

  cacheLife("days"); // Published content, cache longer
  return post.data;
}

Cache Invalidation with cacheTag()

Tag cached entries for on-demand invalidation:

import { cacheTag } from "next/cache";

export async function getSkillById(id: string) {
  "use cache";
  cacheTag("skills", `skill-${id}`);

  return await db.skill.findUnique({ where: { id } });
}

Invalidate with updateTag() in Server Actions (preferred for immediate invalidation):

"use server";
import { updateTag } from "next/cache";

export async function updateSkill(id: string, data: SkillData) {
  await db.skill.update({ where: { id }, data });
  updateTag(`skill-${id}`); // Invalidate specific skill immediately
  updateTag("skills");       // Invalidate all skills
}

updateTag vs revalidateTag:

  • updateTag — Use in Server Actions for read-your-own-writes (user sees changes immediately)
  • revalidateTag — Use in Route Handlers, webhooks, or when stale-while-revalidate is acceptable

❌ Anti-Patterns

Don'tDo Instead
export const revalidate = 3600cacheLife("hours") inside "use cache"
export const dynamic = "force-static"Add "use cache" to component
export const fetchCache = "force-cache"Use "use cache" to control caching
Reading cookies/headers inside cached scopeRead outside, pass as arguments

4. Combined Patterns

The real power comes from combining all three patterns:

Example: E-commerce Product Page

// app/products/[id]/page.tsx (Server Component)
import { Suspense } from "react";
import { ProductDetails } from "@/components/ProductDetails";
import { AddToCartButton } from "@/components/AddToCartButton";
import { RecommendedProducts } from "@/components/RecommendedProducts";
import { ProductSkeleton, RecommendedSkeleton } from "@/components/skeletons";

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  return (
    <div className="grid grid-cols-3 gap-6">
      {/* Cached: Product details rarely change */}
      <div className="col-span-2">
        <Suspense fallback={<ProductSkeleton />}>
          <ProductDetails id={id} />
        </Suspense>
      </div>

      {/* Dynamic: User-specific cart state (Donut Pattern) */}
      <aside>
        <AddToCartButton productId={id} />
      </aside>

      {/* Cached with Suspense: Recommendations can stream in */}
      <div className="col-span-3">
        <Suspense fallback={<RecommendedSkeleton />}>
          <RecommendedProducts productId={id} />
        </Suspense>
      </div>
    </div>
  );
}
// components/ProductDetails.tsx (Cached Server Component)
import { cacheLife, cacheTag } from "next/cache";

export async function ProductDetails({ id }: { id: string }) {
  "use cache";
  cacheLife("hours");
  cacheTag("products", `product-${id}`);

  const product = await db.product.findUnique({ where: { id } });
  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span className="text-2xl font-bold">${product.price}</span>
    </article>
  );
}
// components/AddToCartButton.tsx (Client Component - Donut wrapper)
"use client";
import { useState, useTransition } from "react";
import { addToCart } from "@/actions/cart";

export function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition();
  const [quantity, setQuantity] = useState(1);

  return (
    <form
      action={() => startTransition(() => addToCart(productId, quantity))}
    >
      <input
        type="number"
        value={quantity}
        onChange={(e) => setQuantity(Number(e.target.value))}
        min={1}
      />
      <button disabled={isPending}>
        {isPending ? "Adding..." : "Add to Cart"}
      </button>
    </form>
  );
}

5. Suspense Boundaries Best Practices

When to Use Suspense

ScenarioSuspense Needed?
Cached component ("use cache")Usually not needed (part of static shell)
Dynamic data (user-specific)Yes - shows fallback while loading
Streaming async Server ComponentYes - prevents blocking
Client Component with use()Yes - parent must provide boundary

Granular vs. Coarse Boundaries

// ❌ Coarse: Entire page waits for all data
<Suspense fallback={<FullPageSkeleton />}>
  <Header />
  <MainContent />
  <Sidebar />
</Suspense>

// ✅ Granular: Components load independently
<Header /> {/* Static, no Suspense */}
<Suspense fallback={<ContentSkeleton />}>
  <MainContent /> {/* Async */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
  <Sidebar /> {/* Async, loads parallel to MainContent */}
</Suspense>

6. Debugging Tips

Check Cache Status

In development, Next.js logs cache hits/misses. Look for:

  • CACHE HIT - Served from cache
  • CACHE MISS - Generated fresh and cached
  • CACHE SKIP - Not cacheable (dynamic data accessed)

Common Issues

SymptomLikely CauseFix
Component not cachingAccessing request-specific dataMove cookies/headers read outside cached scope
Stale data after mutationMissing revalidateTag callAdd proper cache tags and revalidate
Hydration mismatchDate/time in cached componentUse cacheLife("seconds") or move to client
Build error with use cacheEdge runtime not supportedUse Node.js runtime only

Verify Static Shell

Run next build and check the output:

  • = Static (rendered at build time)
  • = SSG with dynamic params
  • ƒ = Dynamic (rendered at request time)
  • = Partial Prerendering (static shell + dynamic holes)

Quick Reference

PatternPurposeKey Directive
Data ColocationFetch where data is usedNone (architectural)
Donut PatternServer content in Client wrapper"use client" on wrapper only
Cache ComponentsCache expensive computations"use cache"
FunctionPurpose
cacheLife(profile)Set cache duration
cacheTag(...tags)Tag for targeted invalidation
updateTag(tag)Invalidate immediately (Server Actions only)
revalidateTag(tag)Invalidate with stale-while-revalidate (Route Handlers, webhooks)

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です