パフォーマンス最適化 - Webアプリケーション高速化

20分 で読める | 2025.01.10

パフォーマンス指標

Core Web Vitals

指標説明目標値
LCP最大コンテンツの描画時間< 2.5秒
INP次の描画までのインタラクション遅延< 200ms
CLSレイアウトのずれ< 0.1

バックエンド指標

指標説明目標値
TTFB最初のバイトまでの時間< 200ms
レスポンスタイムAPI応答時間p95 < 500ms
スループット秒間リクエスト数システム依存

フロントエンド最適化

画像最適化

// Next.js Image最適化
import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // LCP画像は優先読み込み
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
/>

// サイズ別画像(srcset)
<picture>
  <source
    srcSet="/image-small.webp 480w, /image-medium.webp 768w, /image-large.webp 1200w"
    type="image/webp"
  />
  <img src="/image-fallback.jpg" alt="..." loading="lazy" />
</picture>

コード分割

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

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,  // クライアントのみ
});

// React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'));

<Suspense fallback={<Loading />}>
  <LazyComponent />
</Suspense>

バンドル最適化

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          charts: ['recharts', 'd3'],
        },
      },
    },
  },
});

レンダリング最適化

// メモ化
const ExpensiveComponent = React.memo(({ data }) => {
  return <div>{/* 重い計算 */}</div>;
});

// useMemo
const sortedData = useMemo(() => {
  return data.sort((a, b) => a.price - b.price);
}, [data]);

// 仮想スクロール
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={400}
  itemCount={10000}
  itemSize={50}
>
  {({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  )}
</FixedSizeList>

バックエンド最適化

N+1問題の解決

// 悪い例(N+1)
const posts = await prisma.post.findMany();
for (const post of posts) {
  post.author = await prisma.user.findUnique({
    where: { id: post.authorId },
  });
}

// 良い例(Eager Loading)
const posts = await prisma.post.findMany({
  include: { author: true },
});

// DataLoaderパターン
const userLoader = new DataLoader(async (ids: string[]) => {
  const users = await prisma.user.findMany({
    where: { id: { in: ids } },
  });
  return ids.map(id => users.find(u => u.id === id));
});

コネクションプール

// Prismaの設定
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
});

// 接続プールサイズ(URL内で設定)
// postgresql://user:pass@host/db?connection_limit=10&pool_timeout=20

レスポンス圧縮

import compression from 'compression';

app.use(compression({
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
  level: 6,  // 圧縮レベル(1-9)
}));

データベース最適化

インデックス戦略

-- 複合インデックス(頻繁なクエリパターンに基づく)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- 部分インデックス(特定条件のみ)
CREATE INDEX idx_active_users ON users(email) WHERE status = 'active';

-- カバリングインデックス
CREATE INDEX idx_products_search ON products(name, price, category_id);

-- インデックス使用状況の確認
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';

クエリ最適化

-- 必要なカラムのみ取得
SELECT id, name, email FROM users WHERE id = 1;
-- NG: SELECT * FROM users WHERE id = 1;

-- ページネーション(Cursor)
SELECT * FROM posts
WHERE id > :last_id
ORDER BY id
LIMIT 20;

-- バッチ処理
UPDATE products SET price = price * 1.1
WHERE category_id = 5
LIMIT 1000;  -- 小分けに実行

キャッシュ戦略

// マルチレイヤーキャッシュ
async function getProduct(id: string): Promise<Product> {
  // L1: メモリキャッシュ
  const memCached = memoryCache.get(`product:${id}`);
  if (memCached) return memCached;

  // L2: Redisキャッシュ
  const redisCached = await redis.get(`product:${id}`);
  if (redisCached) {
    memoryCache.set(`product:${id}`, JSON.parse(redisCached), 60);
    return JSON.parse(redisCached);
  }

  // L3: データベース
  const product = await db.product.findUnique({ where: { id } });

  // キャッシュに保存
  await redis.setex(`product:${id}`, 3600, JSON.stringify(product));
  memoryCache.set(`product:${id}`, product, 60);

  return product;
}

計測とモニタリング

// Server Timing API
app.use((req, res, next) => {
  const start = process.hrtime();

  res.on('finish', () => {
    const [seconds, nanoseconds] = process.hrtime(start);
    const duration = seconds * 1000 + nanoseconds / 1000000;

    res.setHeader('Server-Timing', `total;dur=${duration}`);
  });

  next();
});

関連記事

まとめ

パフォーマンス最適化は、フロントエンド(画像、コード分割)、バックエンド(N+1解決、圧縮)、データベース(インデックス、クエリ)の3層で取り組みます。計測→分析→改善のサイクルを回しましょう。

← 一覧に戻る