はじめに
OGP(Open Graph Protocol)は、ブログや Web サービスで外部リンクのメタ情報を表示する際に広く使われています。
しかし、実装時には 「表示に時間がかかる」「失敗ログが目立つ」「リロードしても改善しない」 といった UX に直結する課題が多く存在します。
本記事では、OGP カードの一般的な課題整理から、従来のクライアント駆動型実装の問題点、改善案の比較、『ビルド時プリフェッチ + 静的キャッシュ』による高速化方法まで、汎用的な観点で解説します。
クライアント駆動型 OGP 取得の課題
多くの Web サービスでは、クライアント側で初期レンダリング後に OGP 情報取得用 API を呼び出す方式が採用されています。
以下は単純にクライアント側で初期レンダリング後に OGP 情報取得用 API を呼び出す方式です。
この方式では、サーバ側の fetch 処理にタイムアウト(例:4.5s)を設けても、外部サイトの応答が遅い場合は skeleton 表示が長時間続きます。
export const DEFAULT_FETCH_TIMEOUT_MS = 4500;
この場合、複数の OGP カードが同時に API リクエストを送るため、外部サイトの応答遅延やタイムアウトが重なると、全体の待機時間が長くなり、UX が大きく悪化します。
これは各 OGP カードがそれぞれ外部サイトへの API リクエストを個別に行い、応答が遅い場合やタイムアウトが発生すると、その分だけ skeleton 表示が続くためです。複数カードが同時にリクエストを送ると、遅延や失敗が重なり、全体の待機時間が長くなりやすくなります。
特に外部サイトの応答速度や可用性に依存するため、ネットワークや先方サーバの状況次第で UX が大きく悪化します。
課題の整理
主な課題は次の通りです。
- 初回アクセス時に毎回 API を叩くため、ネットワーク待ち時間が UX に直結する。
- 同じ URL を複数ページで再利用してもキャッシュが効かず、外部サイトに負荷がかかる。
- 失敗ケースのハンドリング(Timeout, 403, 404)が不十分で、リンク切れ時は永遠にローディングが続く印象を与える。
一時的な対策として、skeleton や URL リンクのみを表示するフォールバックを用いる方法もありますが、前述の問題の根本的な解決には至りません。
改善案の比較
これらの課題に対して以下 3 つの改善方針が考えられます。
| 方針 | 概要 | Pros | Cons |
|---|---|---|---|
| A. ビルド時 Prefetch | ビルド前に OGP 情報を静的収集し JSON キャッシュを生成 | 初期表示が高速化 / ランタイム API 依存を大幅削減 | ビルド時間が延びる / 失敗 URL のケアが必要 |
| B. サーバレス定期ジョブ | サーバレスジョブで定期的に OGP 情報を蓄積 | ビルドと独立して更新可能 / キャッシュを段階的に刷新できる | インフラ構築コスト / 権限管理が増える |
| C. エッジキャッシュ | オンデマンドキャッシュ & 期限管理 | 初回以降は高速 / TTL を柔軟に設定可能 | 初回アクセスは遅いまま / キャッシュ費用が発生 |
本記事では、最小限の仕組み変更で効果が大きい A 案(ビルド時 Prefetch + 静的キャッシュ)を採用し、他案は将来的な拡張として検討することにします。
ビルド時 Prefetch + 静的キャッシュの実装例
ビルド前に対象 URL を抽出し、OGP 情報を取得して JSON キャッシュに保存するスクリプトを用意します。並列実行やエラー回避、TTL ベースのローテーションも組み込むことで、効率的なキャッシュ運用が可能です。
ビルド時に OGP を収集するスクリプト
ビルド前に MDX から URL を抽出 → OGP を取得して(例:.cache/ogp-cache.json)保存するようにしました。スクリプトは並列実行・エラー回避に加えて、TTL ベースのローテーションも担います。
const concurrencyEnv = process.env.OGP_PREFETCH_CONCURRENCY;
const parsedConcurrency = concurrencyEnv
? Number.parseInt(concurrencyEnv, 10)
: DEFAULT_CONCURRENCY;
const concurrency = Number.isFinite(parsedConcurrency)
? Math.max(1, parsedConcurrency)
: DEFAULT_CONCURRENCY;
...
await runWithConcurrency(targets, concurrency, async (url) => {
const result = await fetchOgpData(url);
cache[result.url] = {
...result,
fetchedAt: new Date().toISOString(),
};
if (result.error) {
lastErrorMessage = result.error;
}
});
ビルドコマンドに Prefetch 処理を組み込み、失敗した URL も JSON に記録しておくことで、表示時はフォールバックリンクに切り替えられます。
キャッシュのライフサイクル管理
生成した JSON は GitHub に含めず TTL ベースで自動ローテーション するようにしました。各エントリに fetchedAt を持たせ、既定では 7 日以上経過したものだけ再フェッチします。
-
OGP_PREFETCH_MAX_AGE_DAYS:TTL(日単位)。負数で TTL 無効化、0 で常に再フェッチ -
OGP_PREFETCH_FORCE:TTL を無視して全件再取得 -
OGP_PREFETCH_LIMIT:デバッグ用途でフェッチ件数を絞りたいときに使用
実装はシンプルで、既存キャッシュを読み込んだあとに期限切れ判定を行い、対象のみをワーカにキューイングしています。
const entry = existingCache[url];
if (!entry || maxAgeMs === null) {
// 期限チェック不要。entry が無ければ再フェッチ。
} else if (!entry.fetchedAt || now - Date.parse(entry.fetchedAt) >= maxAgeMs) {
staleCount += 1;
return true; // 対象に含める
}
これにより大量の URL を扱っていても、日常的なビルドでは差分のみ再取得する構成となり、ビルド時間の短縮と安定した表示が実現できます。
クライアントは静的キャッシュを即時利用
クライアント側では、ビルド時に生成した静的 JSON キャッシュを初期状態で利用し、未取得やエラー時のみ AJAX フォールバックを走らせる二段構えにします。
const cachedResponse = STATIC_CACHE[normalizedUrl];
const [ogpData, setOgpData] = useState<OGPResult | null>(() =>
cachedResponse && !cachedResponse.error ? cachedResponse : null
);
const shouldFetch = !cachedResponse || Boolean(cachedResponse?.error);
この実装例は、クライアントコンポーネントでは、ビルド時に吐き出した静的 JSON を読み込み、初期状態で useState にセットしています。エラーや未取得のものだけ AJAX フォールバックを走らせる二段構えです。
アーキテクチャ全体像
ビルドフェーズとランタイムの流れは以下の通りです。
ビルドフェーズとランタイムの挙動をまとめると次のようになります。
キャッシュ初期化の自動化
キャッシュファイルが存在しない場合は、開発・CI 環境の起動時に自動生成する仕組みを追加することで、初回セットアップの失敗を防ぎます。
// scripts/ensure-ogp-cache.ts
if (fs.existsSync(cacheFile)) {
console.log("[ensure-ogp-cache] ... Skipping prefetch.");
return;
}
console.log("[ensure-ogp-cache] cache not found. Running prefetch script...");
await import("./prebuild-ogp");
シーケンスとしては次のような流れになります。
この仕組みによって、CI やローカル開発のどちらでもキャッシュが無いせいで Next.js が起動失敗することが無くなり、初回セットアップが滑らかになりました。
CI/CD への組み込み
CI 環境ではキャッシュディレクトリを保存・再利用することで、ビルド効率をさらに高めることができます。コンテンツ変更時のみ差分フェッチを走らせるのが理想的です。
GitHub Actions などの CI では、.cache ディレクトリを actions/cache で保存しておくとさらに効率的です。hashFiles('data/*/.mdx') をキーにしておけば、コンテンツに変更が無いときはキャッシュを完全に再利用でき、変更があった場合のみ差分フェッチが走ります。ビルドは常に npm run prefetch:ogp を挟むため、TTL 超過分も自然にローテーションされます。
実装のポイント
URL 正規化ロジックの共通化
API・スクリプト・クライアントで重複しがちな URL 補正処理は、共通モジュール化してキャッシュヒット率を高めます。
フェッチレイヤの再利用
OGP 取得処理は共通ライブラリ化し、API ルート・ビルドスクリプト間でタイムアウトやリトライポリシを統一します。
設定可能な同時実行数・リミット
ビルド時間短縮のため、環境変数で同時実行数や取得件数を柔軟に調整できる設計が有効です。
URL 正規化ロジックを共通化
API・スクリプト・クライアントで重複していた URL 補正処理を切り出し、どこから呼んでも同じキーでキャッシュヒットするようにします。
フェッチレイヤの再利用
fetchOgpData は共通ライブラリ化され、API ルートとビルドスクリプトから同じタイムアウト・リトライポリシを共有します。
const response = await fetchWithTimeout(normalizedUrl, {
redirect: "follow",
timeout: options.timeout,
signal: options.signal,
});
設定可能な同時実行とリミット
長時間のビルドを防ぐため、環境変数で挙動をチューニングできるようにしました。
-
OGP_PREFETCH_CONCURRENCY:同時実行数(デフォルト 8) -
OGP_PREFETCH_VERBOSE:ログ量を抑制 -
OGP_PREFETCH_FORCE:既存キャッシュを無視して再取得 -
OGP_PREFETCH_LIMIT:計測やデバッグ用に取得件数を制限
効果測定
改善前後でビルド時 Prefetch の実行時間を比較した例です。
| concurrency | fetched URLs | duration | 備考 |
|---|---|---|---|
| 8 | 719 | 52.76 s | 全件取得(403 / 404 / timeout 含む) |
| 40 | 719 | 10.86 s | 約 4.8 倍高速化 |
TTL による差分更新のみの場合はさらに短縮され、例えば 16 件のみ約 1.36 秒で更新、残りはキャッシュ再利用となります。
静的キャッシュ導入により、クライアント初期表示で API 呼び出しが不要となり、OGP を即座に描画できるようになります。取得失敗 URL もビルド時に判明するため、ログ解析やリンク差し替えも容易です。
改善前後でビルド時 Prefetch の実行時間を比較しました。計測はいずれも 719 件の OGP を OGP_PREFETCH_FORCE=1 で再取得したときの値です。
| concurrency | fetched URLs | duration | 備考 |
|---|---|---|---|
| 8 (従来の直列寄り設定) | 719 | 52.76 s | 403 / 404 / timeout を含む全件取得 |
| 40 (改善後の設定) | 719 | 10.86 s | 約 4.8 倍高速化。ビルド時間への影響が大幅に軽減 |
本番運用に近い設定では TTL によって差分更新だけが走るため、処理時間はさらに短縮されます。直近の増分ビルドでは次のような結果になりました。
| mode | fetched URLs | reused cache | duration | 備考 |
|---|---|---|---|---|
| incremental build | 16 | 719 | 1.36 s | TTL 超過ぶんのみ更新、403 1 件を除外 |
また、静的キャッシュを導入したことで、クライアント初期表示では API 呼び出しが発生せず、OGP を即座に描画できるようになりました。OGP が取得できなかった URL もビルド時に判明するため、ログの解析やリンク差し替えが行いやすくなっています。
まとめ
- ビルド時 Prefetch + 静的キャッシュにより、OGP カードの表示遅延や失敗を大幅に改善できる
- フェッチ処理や URL 補正の共通化で、保守性・再利用性も向上する
- 並列数や TTL の柔軟な設定で、ビルド時間と運用コストの最適化が可能
これに加え、外部ストレージや CDN キャッシュの併用、失敗 URL の自動リトライ、差分更新ジョブなどの拡張も有効です。
- S3 / R2 など外部ストレージへの書き出し、CDN キャッシュとの併用
- 失敗した URL の自動リトライとアラート整備
- 期限切れを検知して差分だけ更新するジョブ(改善案 B の段階的導入)
同様の課題に遭遇した方の参考になれば幸いです。