👏
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取得で初回表示が遅延
解決策
- サーバーサイドでのOGP取得をスキップ
- 新しいAPI エンドポイント
/api/ogp
を作成 - クライアントサイドで遅延読み込み実装
// 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);
}
}
}
};
📈 今後の改善案
- 画像最適化: WebP対応とレスポンシブ画像
- プリフェッチ強化: 内部リンクの積極的プリフェッチ
- Service Worker: オフライン対応とさらなるキャッシュ最適化
- Core Web Vitals: LCP, FID, CLSの継続的改善
🎯 学んだこと
パフォーマンス最適化の原則
- 測定 → 分析 → 実装 → 検証のサイクル
- ユーザー体験を最優先に考慮
- 段階的な改善とテスト
Remix固有の最適化
- Loader関数でのキャッシュヘッダー設定
- サーバー・クライアント分離の重要性
- 環境別最適化の有効性
Cloudflare Pages活用
- CDN-Cache-Controlの活用
- stale-while-revalidateの効果的活用
- エッジでのキャッシュ制御
📚 参考リソース
- Remix Documentation - Caching
- Cloudflare Pages - Cache Control
- Web.dev - Cache-Control
- MDN - stale-while-revalidate
今回の最適化により、ユーザー体験が大幅に向上しました。特にOGPデータの遅延読み込みは、ページ読み込みのブロッキング問題を完全に解決し、開発効率も大幅に改善されました。
継続的な改善とモニタリングを通じて、さらなるパフォーマンス向上を目指します。
Discussion