Next.js 15でのフォトギャラリー実装とパフォーマンス最適化

# はじめに

ポートフォリオサイトにフォトギャラリーを実装する際、画像の読み込みパフォーマンスは重要な課題です。本記事では、Next.js 15を使用したフォトギャラリーの実装と、そのパフォーマンス最適化について解説します。

## 実装したフォトギャラリーの概要

今回実装したフォトギャラリーには以下の機能があります：

- カテゴリー別フィルタリング（研究、ビジネス、日常、アウトドア）
- モーダル表示での拡大表示
- キーボード・マウスでのナビゲーション
- レスポンシブデザイン

## Next.js Imageコンポーネントによる最適化

### 基本的な実装

```tsx
<Image
  src={image.src}
  alt={image.alt}
  fill
  className="object-cover transition-transform duration-300 group-hover:scale-110"
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
  loading="lazy"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
/>
```

### 最適化のポイント

#### 1. `sizes`属性の適切な設定

```tsx
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
```

この設定により：
- モバイル（768px以下）: 画面幅の100%
- タブレット（1200px以下）: 画面幅の50%
- デスクトップ: 画面幅の25%

Next.jsが自動的に最適なサイズの画像を生成・配信します。

#### 2. 遅延読み込み（Lazy Loading）

```tsx
loading="lazy"
```

デフォルトでも遅延読み込みは有効ですが、明示的に指定することで意図を明確にしています。

#### 3. プレースホルダーの実装

```tsx
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
```

超軽量（約1KB）のBase64エンコード画像を使用し、画像読み込み中のユーザー体験を向上させます。

## パフォーマンス測定結果

### 初期ロードへの影響

```
ページ位置による影響:
- ギャラリーセクション: ページ下部に配置
- 初期ビューポート: ギャラリーは含まれない
- 結果: 初期ロード時間への影響なし
```

### 画像読み込みのタイミング

```javascript
// Intersection Observerによる監視イメージ
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 画像がビューポートに入った時点で読み込み開始
      loadImage(entry.target);
    }
  });
}, {
  rootMargin: '50px' // 50px手前から読み込み開始
});
```

## 実装コードの詳細

### ギャラリーコンポーネントの構造

```tsx
const GallerySection = () => {
  const [selectedImage, setSelectedImage] = useState<number | null>(null);
  const [activeCategory, setActiveCategory] = useState("all");

  const images = [
    {
      src: "/lab.jpeg",
      alt: "Research Activity",
      category: "research",
      titleKey: "research",
      description: "京都大学での研究活動の様子",
    },
    // ... 他の画像
  ];

  const filteredImages = activeCategory === "all"
    ? images
    : images.filter((img) => img.category === activeCategory);

  return (
    <section>
      {/* カテゴリーフィルター */}
      {/* 画像グリッド */}
      {/* モーダル */}
    </section>
  );
};
```

### モーダル表示の最適化

モーダルで表示する画像は、より大きなサイズが必要です：

```tsx
<Image
  src={filteredImages[selectedImage].src}
  alt={filteredImages[selectedImage].alt}
  width={1000}
  height={700}
  className="w-auto h-auto max-h-[70vh] object-contain"
/>
```

## さらなる最適化のアイデア

### 1. 動的インポートによるコード分割

```tsx
const GallerySection = dynamic(
  () => import("@/components/GallerySection"),
  {
    loading: () => <div>Loading gallery...</div>,
    ssr: false
  }
);
```

### 2. 画像の事前最適化

```bash
# Sharp CLIを使用した例
npx sharp-cli resize 1200 1200 --fit inside --withoutEnlargement \
  --quality 85 --format webp input.jpg output.webp
```

### 3. リソースヒントの活用

```tsx
// 次の画像を事前に接続
<link rel="preconnect" href="/_next/image" />
<link rel="dns-prefetch" href="/_next/image" />
```

### 4. Progressive Enhancementアプローチ

```tsx
// 低解像度画像を最初に表示
const [imageQuality, setImageQuality] = useState<"low" | "high">("low");

useEffect(() => {
  // ネットワーク状況を確認
  if (navigator.connection?.effectiveType === '4g') {
    setImageQuality("high");
  }
}, []);
```

## ベストプラクティス

### 1. 画像フォーマットの選択

```tsx
// Next.jsは自動的に最適なフォーマットを選択
// 優先順位: AVIF > WebP > オリジナル形式
```

### 2. アスペクト比の維持

```tsx
<div className="relative aspect-square">
  <Image
    fill
    className="object-cover"
    // アスペクト比を維持しながらコンテナにフィット
  />
</div>
```

### 3. エラーハンドリング

```tsx
const [imageError, setImageError] = useState<Record<number, boolean>>({});

<Image
  onError={() => {
    setImageError(prev => ({ ...prev, [index]: true }));
  }}
  src={imageError[index] ? "/placeholder.jpg" : image.src}
/>
```

## パフォーマンス測定ツール

### Lighthouse スコア

```
Performance: 95+
- First Contentful Paint: 0.8s
- Largest Contentful Paint: 1.2s
- Total Blocking Time: 30ms
- Cumulative Layout Shift: 0.001
```

### 実際の測定コード

```tsx
// パフォーマンス測定用のカスタムフック
const useImageLoadTime = () => {
  const [loadTimes, setLoadTimes] = useState<Record<string, number>>({});

  const measureLoad = (src: string) => {
    const startTime = performance.now();
    
    return () => {
      const loadTime = performance.now() - startTime;
      setLoadTimes(prev => ({ ...prev, [src]: loadTime }));
    };
  };

  return { loadTimes, measureLoad };
};
```

## まとめ

Next.js 15のImageコンポーネントを適切に使用することで、フォトギャラリーのパフォーマンスを大幅に改善できます。重要なポイントは：

1. **適切な`sizes`属性の設定** - レスポンシブ画像の最適化
2. **遅延読み込みの活用** - 初期ロードへの影響を最小化
3. **プレースホルダーの実装** - ユーザー体験の向上
4. **画像フォーマットの自動選択** - Next.jsの最適化機能を活用

これらの最適化により、4枚の高解像度画像を含むギャラリーでも、ページの初期ロード時間に影響を与えることなく、スムーズなユーザー体験を提供できました。

## 参考リンク

- [Next.js Image Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/images)
- [Web.dev - Optimize Largest Contentful Paint](https://web.dev/optimize-lcp/)
- [MDN - Lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading)