mcpField
P
performance-optimization

performance-optimization

github.com/jmsktm開発1066 回インストール41 週間前に更新
リポジトリ

performance-optimization について

performance-optimization は、Next.js アプリの性能改善に取り組む開発者向けのスキルです。Core Web Vitals、レンダリング、キャッシュ、バンドル、データベースクエリの最適化を扱います。

主な機能

  1. 01Core Web Vitalsの最適化ガイド
  2. 02画像とフォントの最適化
  3. 03動的インポートとコード分割
  4. 04キャッシュとタグベースの再検証
  5. 05バンドル分析とツリーシェイキング
  6. 06データベースクエリとレンダリング戦略

活用シーン

  1. 01ページ読み込みの遅さの調査
  2. 02JavaScriptバンドルサイズの削減
  3. 03画像とフォント配信の改善
  4. 04データ取得とデータベースクエリの調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jmsktm/claude-settings --skill performance-optimization

インストール後、Claude Code で/performance-optimizationを入力して読み込まれたか確認します。

詳細

パフォーマンス最適化スキル

概要

このスキルは、Next.js アプリケーションのパフォーマンスを最大限に高めるのに役立ちます。画像の最適化からコード分割まで、優れた Core Web Vitals スコアを達成するために必要な手法を網羅しています。

Core Web Vitals

1. 最大コンテンツ描画(LCP)

目標:< 2.5s

最適化方法:

  • 画像には next/image を使用する
  • 適切なキャッシュを実装する
  • 静的アセットには CDN を使用する
  • サーバーの応答時間を最適化する
  • レンダリングをブロックするリソースを減らす
2. 初回入力遅延(FID)/次の描画までのインタラクション(INP)

目標:< 100ms / < 200ms

最適化方法:

  • JavaScript の実行を最小限に抑える
  • 大きなバンドルをコード分割する
  • 負荷の高いタスクには Web Workers を使用する
  • 重要度の低い JavaScript の読み込みを遅らせる
  • イベントハンドラーを最適化する
3. 累積レイアウトシフト(CLS)

目標:< 0.1

最適化方法:

  • 画像のサイズを指定する
  • 広告用のスペースを確保する
  • 既存のコンテンツより上にコンテンツを挿入しない
  • レイアウトプロパティの代わりに transform を使用する

画像の最適化

Next.js Image コンポーネント
import Image from 'next/image'

// ✅ Optimized
export function OptimizedImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority  // For above-fold images
      quality={85}  // Default: 75
      placeholder="blur"
      blurDataURL="data:image/..." // Or import for static
    />
  )
}

// For external images, configure domains
// next.config.js
module.exports = {
  images: {
    domains: ['example.com'],
    // Or use remotePatterns for more control
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },
}
レスポンシブ画像
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  style={{ objectFit: 'cover' }}
  priority
/>
画像フォーマット
// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
  },
}

フォントの最適化

next/font の使用
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
}
ローカルフォント
import localFont from 'next/font/local'

const customFont = localFont({
  src: './fonts/custom-font.woff2',
  display: 'swap',
  variable: '--font-custom',
})

コード分割

動的インポート
import dynamic from 'next/dynamic'

// Load component only when needed
const HeavyComponent = dynamic(() => import('@/components/heavy-component'), {
  loading: () => <p>Loading...</p>,
  ssr: false,  // Disable SSR for this component
})

export function Page() {
  return (
    <div>
      <HeavyComponent />
    </div>
  )
}
条件付き読み込み
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('@/components/chart'), {
  ssr: false,
})

export function Dashboard() {
  const [showChart, setShowChart] = useState(false)

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && <Chart />}
    </div>
  )
}
名前付きエクスポート
const ComponentA = dynamic(() =>
  import('@/components/bundle').then((mod) => mod.ComponentA)
)

React の最適化

React.memo
import { memo } from 'react'

// Only re-renders if props change
const ExpensiveComponent = memo(function ExpensiveComponent({
  data,
}: {
  data: Data
}) {
  return <div>{/* Expensive rendering */}</div>
})

// Custom comparison
const MemoizedComponent = memo(
  Component,
  (prevProps, nextProps) => {
    return prevProps.id === nextProps.id
  }
)
useMemo
'use client'
import { useMemo } from 'react'

export function DataTable({ items }: { items: Item[] }) {
  // Only recalculate when items change
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.name.localeCompare(b.name))
  }, [items])

  return (
    <table>
      {sortedItems.map((item) => (
        <tr key={item.id}>
          <td>{item.name}</td>
        </tr>
      ))}
    </table>
  )
}
useCallback
'use client'
import { useCallback, useState } from 'react'

export function Parent() {
  const [count, setCount] = useState(0)

  // Stable function reference
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])

  return <Child onClick={handleClick} />
}

キャッシュ戦略

API ルートのキャッシュ
// app/api/data/route.ts
export async function GET() {
  const data = await fetchData()

  return Response.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
    },
  })
}
データ取得のキャッシュ
// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 },
})

// Never cache (always fresh)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
})

// Cache forever
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache',
})
タグベースの再検証
// Tag the fetch
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
})

// Revalidate all 'posts' fetches
import { revalidateTag } from 'next/cache'

export async function POST() {
  // Mutate data
  await createPost()
  // Revalidate
  revalidateTag('posts')
}

バンドルの最適化

バンドルの分析
# Add to package.json
{
  "scripts": {
    "analyze": "ANALYZE=true next build"
  }
}

# Install bundle analyzer
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // Your config
})
ツリーシェイキング
// ❌ Bad - Imports entire library
import _ from 'lodash'

// ✅ Good - Only imports what you need
import debounce from 'lodash/debounce'
未使用の依存関係の削除
# Find unused dependencies
npx depcheck

# Remove them
npm uninstall unused-package

ストリーミングと Suspense

コンポーネントのストリーミング
// app/dashboard/page.tsx
import { Suspense } from 'react'

async function SlowComponent() {
  const data = await slowFetch()
  return <div>{data}</div>
}

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Loading />}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}
複数の Suspense 境界
export default function Page() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <Content />
      </Suspense>

      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
    </div>
  )
}

データベースクエリの最適化

Prisma の効率的な使用
// ❌ Bad - N+1 query problem
const users = await prisma.user.findMany()
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { userId: user.id } })
}

// ✅ Good - Single query with include
const users = await prisma.user.findMany({
  include: {
    posts: true,
  },
})

// ✅ Better - Select only what you need
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    posts: {
      select: {
        id: true,
        title: true,
      },
    },
  },
})
データベースインデックス
model Post {
  id        String   @id @default(cuid())
  title     String
  userId    String
  createdAt DateTime @default(now())

  // Add indexes for frequently queried fields
  @@index([userId])
  @@index([createdAt])
  @@index([userId, createdAt])
}

プリレンダリング戦略

静的サイト生成(SSG)
// Fully static - generated at build time
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>{/* Render data */}</div>
}
インクリメンタル静的再生成(ISR)
// Revalidate every 60 seconds
export const revalidate = 60

export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>{/* Render data */}</div>
}
動的レンダリング
// Force dynamic rendering
export const dynamic = 'force-dynamic'

export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store',
  })
  return <div>{/* Render data */}</div>
}

遅延読み込み

画像
<Image
  src="/image.jpg"
  alt="Image"
  width={500}
  height={300}
  loading="lazy"  // Default behavior
/>
スクロール時のコンポーネント
'use client'
import { useEffect, useState, useRef } from 'react'
import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('@/components/heavy'))

export function LazySection() {
  const [isVisible, setIsVisible] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true)
          observer.disconnect()
        }
      },
      { threshold: 0.1 }
    )

    if (ref.current) {
      observer.observe(ref.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <div ref={ref}>
      {isVisible ? <HeavyComponent /> : <div>Loading...</div>}
    </div>
  )
}

パフォーマンスのモニタリング

パフォーマンスの測定
'use client'
import { useEffect } from 'react'

export function PerformanceMonitor() {
  useEffect(() => {
    // Measure LCP
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime)
    }).observe({ type: 'largest-contentful-paint', buffered: true })

    // Measure FID
    new PerformanceObserver((list) => {
      const entries = list.getEntries()
      entries.forEach((entry) => {
        console.log('FID:', entry.processingStart - entry.startTime)
      })
    }).observe({ type: 'first-input', buffered: true })

    // Measure CLS
    let clsScore = 0
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsScore += entry.value
        }
      }
      console.log('CLS:', clsScore)
    }).observe({ type: 'layout-shift', buffered: true })
  }, [])

  return null
}

ベストプラクティスのチェックリスト

  • すべての画像に next/image を使用する
  • next/font でフォントを最適化する
  • 大きなコンポーネントにコード分割を実装する
  • 負荷の高いコンポーネントに React.memo を使用する
  • 適切なキャッシュ戦略を実装する
  • Suspense 境界を設定する
  • データベースクエリを最適化する
  • 準静的コンテンツに ISR を有効にする
  • ファーストビュー以降のコンテンツを遅延読み込みする
  • Core Web Vitals をモニタリングする
  • JavaScript バンドルのサイズを最小限に抑える
  • 静的アセットに CDN を使用する
  • 適切な読み込み状態を実装する
  • 低速な 3G 接続でテストする

このスキルを使用するタイミング

次のような場合にこのスキルを呼び出します:

  • ページの読み込み時間を最適化するとき
  • Core Web Vitals のスコアを改善するとき
  • バンドルサイズを削減するとき
  • パフォーマンスの問題をデバッグするとき
  • キャッシュ戦略を設定するとき
  • 遅延読み込みを実装するとき
  • 画像やフォントを最適化するとき
  • データベースクエリのパフォーマンスを改善するとき

コメント