mcpField
P
performance-engineering

performance-engineering

github.com/travisjneuman開発106139 回インストール1 日前に更新
リポジトリ

performance-engineering について

performance-engineeringは、Webアプリ開発者向けのパフォーマンス測定・最適化ガイドです。Core Web Vitals、コード分割、キャッシュ、データベースクエリ、負荷テストを扱い、測定に基づく改善を重視します。

主な機能

  1. 01Core Web Vitalsの測定
  2. 02JavaScriptバンドル分析とコード分割
  3. 03画像とクリティカルパスの最適化
  4. 04ブラウザー、サーバー、CDNのキャッシュ設定
  5. 05データベースクエリ性能の分析
  6. 06k6とArtilleryによる負荷テスト

活用シーン

  1. 01Core Web Vitalsの目標未達の調査
  2. 02増加するフロントエンドバンドルの縮小
  3. 03ページ表示と操作応答の改善
  4. 04負荷テストによるトラフィック急増への準備

インストール

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

$npx skills add https://github.com/travisjneuman/.claude --skill performance-engineering

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

詳細

パフォーマンスエンジニアリング

概要

このスキルでは、フルスタックにわたるウェブアプリケーションのパフォーマンスの測定、最適化、維持について扱います。Core Web Vitals(LCP、INP、CLS)、バンドルの分析とコード分割、画像の最適化、ブラウザーおよびサーバーサイドのキャッシュ、CDN の設定、データベースクエリのパフォーマンス、k6 と Artillery による負荷テストを対象とします。

Core Web Vitals のスコアが目標を下回る場合、バンドルサイズが増加している場合、ページの読み込み時間が悪化している場合、またはトラフィックの急増に備える場合に、このスキルを使用してください。パフォーマンスの改善は測定に基づいて行う必要があります。まずプロファイルを取り、次に最適化し、最後に効果を測定してください。


基本原則

  1. 最適化の前に測定する - 最適化コードを書く前に、実データ(RUM、Lighthouse、DevTools)を使ってプロファイルを取ってください。パフォーマンスのボトルネックに関する直感は、たいてい間違っています。
  2. すべてに予算を設定する - バンドルサイズ(< 200kb JS)、LCP(< 2.5s)、INP(< 200ms)、CLS(< 0.1)のパフォーマンス予算を設定してください。レビューでは、変更内容が予算内か確認してください。
  3. クリティカルパスを最適化する - ユーザーがコンテンツを表示し、操作できるようになるまでの妨げとなる要素に集中してください。それ以外は後から読み込めます。
  4. 積極的にキャッシュし、正確に無効化する - 静的アセットには不変のハッシュを、動的コンテンツには短い TTL を使い、両方の利点を得るには stale-while-revalidate を使ってください。
  5. 現実的な規模を想定する - 同時接続ユーザーが 10 人の小規模なデータセットではなく、本番環境に近いデータ量とトラフィックを基準に設計を評価してください。負荷テストはユーザーから依頼された場合にのみ実施してください。

主なパターン

パターン 1:Core Web Vitals のモニタリング

使用する場面: すべての本番ウェブアプリケーション。これらの指標は SEO ランキングとユーザーエクスペリエンスに直接影響します。

実装:

// Real User Monitoring (RUM) with web-vitals library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";

interface VitalMetric {
  name: string;
  value: number;
  rating: "good" | "needs-improvement" | "poor";
  navigationType: string;
}

function sendToAnalytics(metric: VitalMetric) {
  // Send to your analytics endpoint
  navigator.sendBeacon("/api/vitals", JSON.stringify({
    ...metric,
    url: window.location.href,
    userAgent: navigator.userAgent,
    connectionType: (navigator as unknown as { connection?: { effectiveType: string } })
      .connection?.effectiveType ?? "unknown",
    timestamp: Date.now(),
  }));
}

// Capture all Core Web Vitals
onLCP((metric) => sendToAnalytics({
  name: "LCP",
  value: metric.value,
  rating: metric.rating,
  navigationType: metric.navigationType,
}));

onINP((metric) => sendToAnalytics({
  name: "INP",
  value: metric.value,
  rating: metric.rating,
  navigationType: metric.navigationType,
}));

onCLS((metric) => sendToAnalytics({
  name: "CLS",
  value: metric.value,
  rating: metric.rating,
  navigationType: metric.navigationType,
}));

onFCP((metric) => sendToAnalytics({
  name: "FCP",
  value: metric.value,
  rating: metric.rating,
  navigationType: metric.navigationType,
}));

onTTFB((metric) => sendToAnalytics({
  name: "TTFB",
  value: metric.value,
  rating: metric.rating,
  navigationType: metric.navigationType,
}));

Lighthouse CI やその他の新しい CI ジョブは、ユーザーから依頼された場合にのみ追加してください。

// Lighthouse CI configuration
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: [
        "http://localhost:3000/",
        "http://localhost:3000/dashboard",
        "http://localhost:3000/pricing",
      ],
      numberOfRuns: 3,
    },
    assert: {
      assertions: {
        "categories:performance": ["error", { minScore: 0.9 }],
        "categories:accessibility": ["error", { minScore: 0.95 }],
        "first-contentful-paint": ["warn", { maxNumericValue: 1800 }],
        "largest-contentful-paint": ["error", { maxNumericValue: 2500 }],
        "interactive": ["error", { maxNumericValue: 3800 }],
        "cumulative-layout-shift": ["error", { maxNumericValue: 0.1 }],
        "total-byte-weight": ["warn", { maxNumericValue: 500000 }],
      },
    },
    upload: {
      target: "temporary-public-storage",
    },
  },
};

理由: ラボ指標(Lighthouse)はデプロイ前にリグレッションを検出します。フィールド指標(RUM)は、さまざまなデバイスやネットワークにおける実際のユーザーエクスペリエンスを示します。どちらも必要です。ラボ指標は防止に、フィールド指標は実態の把握に役立ちます。


パターン 2:バンドルの最適化とコード分割

使用する場面: JavaScript バンドルサイズが gzip 圧縮後に 200kb を超える場合、または初期読み込みにユーザーがまだ訪問していないルートのコードが含まれている場合。

実装:

// Next.js - Dynamic imports for route-based code splitting
import dynamic from "next/dynamic";

// Heavy component loaded only when needed
const RichTextEditor = dynamic(() => import("@/components/RichTextEditor"), {
  loading: () => <div className="editor-skeleton" aria-busy="true">Loading editor...</div>,
  ssr: false, // Client-only component
});

const ChartDashboard = dynamic(() => import("@/components/ChartDashboard"), {
  loading: () => <ChartSkeleton />,
});

// Conditional feature loading
function ProjectPage({ project }: { project: Project }) {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <div>
      <h1>{project.name}</h1>
      <button onClick={() => setShowEditor(true)}>Edit Description</button>
      {showEditor && <RichTextEditor content={project.description} />}
    </div>
  );
}
// webpack-bundle-analyzer integration
// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer({
  // Analyze specific packages for tree-shaking
  experimental: {
    optimizePackageImports: ["lodash-es", "date-fns", "@mui/material", "lucide-react"],
  },
});
# Analyze bundle
ANALYZE=true npm run build

# Check specific import costs
npx import-cost  # VS Code extension alternative
npx source-map-explorer .next/static/chunks/*.js

理由: JavaScript は 1 キロバイト増えるごとに、ユーザーのデバイスでの解析時間、コンパイル時間、実行時間が増えます。コード分割によって、ユーザーが現在表示している画面に必要なコードだけをダウンロードするようにできます。エディター、チャート、マップなどの重いコンポーネントを遅延読み込みすれば、初期バンドルを軽量に保てます。


パターン 3:画像の最適化

使用する場面: 画像があるページ(ほとんどのページが該当します)。通常、画像はページ上で最も容量の大きいアセットです。

実装:

// Next.js Image component with proper optimization
import Image from "next/image";

// Responsive hero image
function HeroSection() {
  return (
    <section>
      <Image
        src="/hero.jpg"
        alt="Team collaborating on a project"
        width={1920}
        height={1080}
        priority  // LCP element - skip lazy loading
        sizes="100vw"
        quality={85}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."  // Low-quality placeholder
      />
    </section>
  );
}

// Responsive card image
function ProductCard({ product }: { product: Product }) {
  return (
    <article>
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={400}
        height={300}
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        loading="lazy"  // Below the fold
      />
      <h3>{product.name}</h3>
    </article>
  );
}
// Sharp-based image processing pipeline for user uploads
import sharp from "sharp";

interface ImageVariant {
  width: number;
  suffix: string;
  quality: number;
}

const variants: ImageVariant[] = [
  { width: 320, suffix: "sm", quality: 80 },
  { width: 640, suffix: "md", quality: 80 },
  { width: 1280, suffix: "lg", quality: 85 },
  { width: 1920, suffix: "xl", quality: 85 },
];

async function processUploadedImage(
  buffer: Buffer,
  filename: string
): Promise<string[]> {
  const urls: string[] = [];

  for (const variant of variants) {
    // Generate WebP (best compression)
    const webp = await sharp(buffer)
      .resize(variant.width, null, { withoutEnlargement: true })
      .webp({ quality: variant.quality })
      .toBuffer();

    // Generate AVIF (even better compression, slower to encode)
    const avif = await sharp(buffer)
      .resize(variant.width, null, { withoutEnlargement: true })
      .avif({ quality: variant.quality - 10 })
      .toBuffer();

    const webpUrl = await uploadToStorage(webp, `${filename}-${variant.suffix}.webp`);
    const avifUrl = await uploadToStorage(avif, `${filename}-${variant.suffix}.avif`);

    urls.push(webpUrl, avifUrl);
  }

  return urls;
}

理由: ほとんどのサイトでは、画像がページ容量の 50% 以上を占めます。最新のフォーマット(WebP、AVIF)は、JPEG と比べてサイズを 25〜50% 削減できます。レスポンシブな sizes 属性を指定すると、モバイルデバイスがデスクトップ向けサイズの画像をダウンロードするのを防げます。LCP 画像に priority を指定すると、最も重要な視覚要素の遅延読み込みがなくなります。


パターン 4:キャッシュ戦略

使用する場面: どのアプリケーションでもキャッシュの恩恵を受けられます。重要なのは、どのレイヤーで、どの TTL を設定するかです。

実装:

// HTTP cache headers for different asset types
// next.config.js
module.exports = {
  async headers() {
    return [
      // Static assets with content hashes - cache forever
      {
        source: "/_next/static/:path*",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=31536000, immutable",
          },
        ],
      },
      // API responses - short cache with revalidation
      {
        source: "/api/:path*",
        headers: [
          {
            key: "Cache-Control",
            value: "public, s-maxage=60, stale-while-revalidate=300",
          },
        ],
      },
      // HTML pages - always revalidate
      {
        source: "/:path*",
        headers: [
          {
            key: "Cache-Control",
            value: "public, max-age=0, must-revalidate",
          },
        ],
      },
    ];
  },
};
// Server-side caching with Redis
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface CacheOptions {
  ttlSeconds: number;
  staleSeconds?: number; // Serve stale while revalidating
}

async function cached<T>(
  key: string,
  fetcher: () => Promise<T>,
  options: CacheOptions
): Promise<T> {
  const cached = await redis.get(key);

  if (cached) {
    const { data, expiry } = JSON.parse(cached);
    const isStale = Date.now() > expiry;

    if (!isStale) {
      return data as T;
    }

    // Stale-while-revalidate: return stale data, refresh in background
    if (options.staleSeconds) {
      const staleDeadline = expiry + options.staleSeconds * 1000;
      if (Date.now() < staleDeadline) {
        // Revalidate in background (fire-and-forget)
        refreshCache(key, fetcher, options).catch(console.error);
        return data as T;
      }
    }
  }

  return refreshCache(key, fetcher, options);
}

async function refreshCache<T>(
  key: string,
  fetcher: () => Promise<T>,
  options: CacheOptions
): Promise<T> {
  const data = await fetcher();
  const expiry = Date.now() + options.ttlSeconds * 1000;
  const totalTtl = options.ttlSeconds + (options.staleSeconds ?? 0);

  await redis.setex(key, totalTtl, JSON.stringify({ data, expiry }));
  return data;
}

// Usage
const products = await cached(
  `products:category:${categoryId}`,
  () => db.products.findMany({ where: { categoryId } }),
  { ttlSeconds: 300, staleSeconds: 600 }
);

理由: キャッシュを使うと、重複した計算やネットワークリクエストをなくせます。コンテンツハッシュを持つ不変の静的アセットは、安全に無期限でキャッシュできます。stale-while-revalidate を使えば、ユーザーには即座に応答を返しながら、バックグラウンドでデータを最新に保てるため、体感パフォーマンスを最大限に高められます。


パターン 5:k6 による負荷テスト

使用する場面: 大規模なリリースや移行の前、またはパフォーマンスのベースラインを確立するためなど、ユーザーから負荷テストを依頼された場合です。

実装:

// load-test.js - k6 load test script
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate, Trend } from "k6/metrics";

const errorRate = new Rate("errors");
const apiDuration = new Trend("api_duration", true);

export const options = {
  stages: [
    { duration: "2m", target: 50 },   // Ramp up
    { duration: "5m", target: 50 },   // Steady state
    { duration: "2m", target: 200 },  // Spike
    { duration: "5m", target: 200 },  // Sustained spike
    { duration: "2m", target: 0 },    // Ramp down
  ],
  thresholds: {
    http_req_duration: ["p(95)<500", "p(99)<1000"], // 95th < 500ms, 99th < 1s
    errors: ["rate<0.01"],                          // Error rate < 1%
    api_duration: ["avg<200"],                      // Average API < 200ms
  },
};

const BASE_URL = __ENV.BASE_URL || "http://localhost:3000";

export default function () {
  // Simulate realistic user journey
  const homeRes = http.get(`${BASE_URL}/`);
  check(homeRes, {
    "home status 200": (r) => r.status === 200,
    "home load < 2s": (r) => r.timings.duration < 2000,
  });
  errorRate.add(homeRes.status >= 400);

  sleep(1);

  // API call
  const apiRes = http.get(`${BASE_URL}/api/products?category=electronics`, {
    headers: { "Content-Type": "application/json" },
  });
  check(apiRes, {
    "api status 200": (r) => r.status === 200,
    "api has results": (r) => JSON.parse(r.body).length > 0,
  });
  apiDuration.add(apiRes.timings.duration);
  errorRate.add(apiRes.status >= 400);

  sleep(Math.random() * 3); // Random think time
}
# Run load test
k6 run load-test.js

# Run with custom base URL
k6 run -e BASE_URL=https://staging.example.com load-test.js

# Output results to JSON for analysis
k6 run --out json=results.json load-test.js

理由: 負荷テストでは、同時実行時にのみ発生するボトルネックを特定できます。たとえば、データベース接続プールの枯渇、メモリーリーク、ロック競合、連鎖的なタイムアウトなどです。現実的なトラフィックパターン(段階的な負荷の増加、持続的な負荷、急増)でテストすることで、システムが実環境の条件に対応できることを確認できます。


パフォーマンス予算の参考値

指標良好要改善不良
LCP< 2.5s2.5s - 4.0s> 4.0s
INP< 200ms200ms - 500ms> 500ms
CLS< 0.10.1 - 0.25> 0.25
FCP< 1.8s1.8s - 3.0s> 3.0s
TTFB< 800ms800ms - 1800ms> 1800ms
JS 合計< 200kb gz200-400kb gz> 400kb gz
ページの合計サイズ< 1MB1-3MB> 3MB

アンチパターン

アンチパターン問題点より良い方法
計測せずに最適化するボトルネックではない箇所に労力を費やしますまずプロファイリングし、計測で特定したホットスポットを最適化します
すべての JS を最初に読み込む使用されないコードによって操作性が損なわれますルートごとにコードを分割し、重いコンポーネントは遅延読み込みします
width/height を指定せずに画像を使用するレイアウトシフト(CLS)が発生します必ず寸法を指定するか、アスペクト比を使用します
すべてに Cache-Control: no-cache を指定するリクエストのたびにオリジンサーバーへアクセスしますAPI には stale-while-revalidate を、ハッシュ付きアセットには immutable を使用します
Web フォントを同期的に読み込むテキストの描画が妨げられます(FOIT)font-display: swap または optional を使用します
<head> 内にサードパーティスクリプトを置くページの描画が妨げられますasync または defer を指定して読み込むか、操作後に読み込みます
N+1 データベースクエリデータ量の増加に伴ってレイテンシが増大しますイーガーローディング(Prisma の include)やクエリのバッチ処理を使用します

チェックリスト

  • フィールド(RUM)とラボ(Lighthouse)で Core Web Vitals を計測している
  • パフォーマンス予算を定義し、レビューで確認している
  • バンドルを分析し、ルートごとにコードを分割している
  • 画像を最適化している(WebP/AVIF、レスポンシブサイズ、遅延読み込み)
  • LCP 画像に priority/fetchpriority="high" を指定している
  • アセットの種類ごとにキャッシュヘッダーを設定している
  • 高コストなクエリにサーバーサイドキャッシュを使用している(Redis またはインメモリー)
  • サードパーティスクリプトを監査し、非同期で読み込んでいる
  • データベースクエリを分析している(N+1 がなく、適切なインデックスがある)
  • 現実的なトラフィックに対する負荷時の挙動を検討している(負荷テストは依頼があった場合のみ)
  • カスタム Web フォントに font-display: swap を設定している

関連リソース

  • スキル: monitoring-observability(パフォーマンス監視)、serverless-development(コールドスタートの最適化)
  • ルール: docs/reference/checklists/ui-visual-changes.md(視覚的なパフォーマンス)
  • ルール: docs/reference/stacks/react-typescript.md(React のパフォーマンスパターン)

コメント