👏

Remix + Cloudflare Pagesパフォーマンス最適化: キャッシュ戦略とOGP遅延読み込み実装記録

に公開

RemixとCloudflare Pagesで構築したブログアプリケーションのパフォーマンス最適化を実施しました。今回実装した施策の詳細を記録として残します。

🚀 実装した最適化施策

1. Cloudflare最適化キャッシュ戦略

各ページタイプに応じて適切なキャッシュヘッダーを設定:

ホームページ (app/routes/_index.tsx)

headers: {
  "Cache-Control": "public, max-age=60, stale-while-revalidate=300",
  "CDN-Cache-Control": "public, s-maxage=300, stale-while-revalidate=3600",
}

記事ページ (app/routes/articles.$slug.tsx)

headers: {
  "Cache-Control": "public, max-age=300, stale-while-revalidate=1800",
  "CDN-Cache-Control": "public, s-maxage=1800, stale-while-revalidate=86400",
}

OGP API (app/routes/api.ogp.tsx)

headers: {
  "Cache-Control": "public, max-age=3600, stale-while-revalidate=7200",
  "CDN-Cache-Control": "public, s-maxage=7200, stale-while-revalidate=86400",
}

2. OGPデータの遅延読み込み

従来のサーバーサイドでのOGP取得によるページ読み込みブロックを解決:

問題

  • OGPデータ取得のネットワークタイムアウトでページが読み込めない
  • 複数URLのOGP取得で初回表示が遅延

解決策

  1. サーバーサイドでのOGP取得をスキップ
  2. 新しいAPI エンドポイント /api/ogp を作成
  3. クライアントサイドで遅延読み込み実装
// app/components/OGPLinkCard.tsx
useEffect(() => {
  if (!ogpData) {
    fetch(`/api/ogp?url=${encodeURIComponent(url)}`)
      .then(res => res.json())
      .then(result => {
        if (result.ogpData) {
          setData(result.ogpData);
        } else {
          setData(null);
        }
      })
      .catch(error => {
        console.error('OGP fetch failed:', error);
        setData(null);
      })
      .finally(() => setLoading(false));
  }
}, [url, ogpData]);

3. ローカル開発環境の改善

MDXファイル直接読み込み機能

ローカル環境では動的にMDXファイルから記事を読み込み:

// app/routes/articles.$slug.tsx
const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1";

if (isLocal) {
  // ローカル環境では直接MDXファイルから読み込み
  const { getArticleFromMDX } = await import("~/utils/markdown.server");
  article = await getArticleFromMDX(slug);
} else {
  // 本番環境では既存のJSONから読み込み
  const res = await fetch(`${url.origin}/data/articles.json`);
  const data = await res.json();
  article = data[slug];
}

管理エディターの大幅改善

  • モダンなツールバーUI
  • ドラッグ&ドロップ画像アップロード
  • クリップボードからの画像ペースト
  • リアルタイムプレビュー
  • シンタックスハイライト対応

📊 パフォーマンス改善結果

キャッシュ戦略の効果

  • 初回読み込み後: 劇的な高速化
  • CDN効果: グローバルなレスポンス向上
  • stale-while-revalidate: ユーザー体験向上

OGP遅延読み込みの効果

  • ページ読み込みブロック: 完全に解消
  • 初回表示速度: 大幅な高速化
  • エラー耐性: ネットワークエラーによる影響なし

🛠 技術的な実装ポイント

1. サーバー・クライアント分離

// サーバーサイドのみで実行される関数の動的インポート
if (typeof window === 'undefined') {
  const { fetchOGPData } = await import("~/utils/ogp-fetcher.server");
  // サーバーサイド処理
}

2. エラーハンドリングとタイムアウト

// タイムアウト付きフェッチ
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

const response = await fetch(url, {
  signal: controller.signal,
  headers: { 'User-Agent': 'Mozilla/5.0 (compatible; OGPBot/1.0)' }
});

clearTimeout(timeoutId);

3. 環境別最適化

  • ローカル: 開発効率重視の動的読み込み
  • 本番: パフォーマンス重視の事前ビルド + キャッシュ

🔧 開発環境の改善

管理画面エディター強化

// ツールバー機能
const insertAtCursor = (insertText: string) => {
  const textarea = textareaRef.current;
  const start = textarea.selectionStart;
  const end = textarea.selectionEnd;
  const newText = text.substring(0, start) + insertText + text.substring(end);
  setText(newText);
  
  setTimeout(() => {
    textarea.focus();
    textarea.setSelectionRange(start + insertText.length, start + insertText.length);
  }, 0);
};

ドラッグ&ドロップとペースト機能

// ドラッグ&ドロップ処理
const handleDrop = async (e: React.DragEvent) => {
  e.preventDefault();
  const files = Array.from(e.dataTransfer.files);
  
  for (const file of files) {
    if (file.type.startsWith('image/')) {
      await onUpload(file);
    }
  }
};

// クリップボードから画像ペースト
const handlePaste = async (e: React.ClipboardEvent) => {
  const items = e.clipboardData.items;
  
  for (const item of items) {
    if (item.type.startsWith('image/')) {
      e.preventDefault();
      const file = item.getAsFile();
      if (file) {
        await onUpload(file);
      }
    }
  }
};

📈 今後の改善案

  1. 画像最適化: WebP対応とレスポンシブ画像
  2. プリフェッチ強化: 内部リンクの積極的プリフェッチ
  3. Service Worker: オフライン対応とさらなるキャッシュ最適化
  4. Core Web Vitals: LCP, FID, CLSの継続的改善

🎯 学んだこと

パフォーマンス最適化の原則

  • 測定 → 分析 → 実装 → 検証のサイクル
  • ユーザー体験を最優先に考慮
  • 段階的な改善とテスト

Remix固有の最適化

  • Loader関数でのキャッシュヘッダー設定
  • サーバー・クライアント分離の重要性
  • 環境別最適化の有効性

Cloudflare Pages活用

  • CDN-Cache-Controlの活用
  • stale-while-revalidateの効果的活用
  • エッジでのキャッシュ制御

📚 参考リソース


今回の最適化により、ユーザー体験が大幅に向上しました。特にOGPデータの遅延読み込みは、ページ読み込みのブロッキング問題を完全に解決し、開発効率も大幅に改善されました。

継続的な改善とモニタリングを通じて、さらなるパフォーマンス向上を目指します。

Discussion