Sol SSG

ISR (Incremental Static Regeneration)

ISRを使用すると、サイト全体を再ビルドすることなく、デプロイ後に静的ページを更新できます。

概要

ISRは静的生成の利点と動的コンテンツの鮮度を組み合わせます:

  1. ビルド時に静的生成 - ページはビルド時に事前レンダリング

  2. キャッシュ配信 - 最小限のレイテンシでキャッシュから配信

  3. バックグラウンド再検証 - 古いコンテンツは非同期で再生成をトリガー

  4. 再ビルド不要 - フルサイト再ビルドなしでコンテンツ更新

動作の仕組み

リクエスト → キャッシュ確認
               ↓
         ┌─────┴─────┐
         │           │
      Fresh?      Stale?      Miss?
         │           │           │
      キャッシュ   キャッシュ +    生成
       を返す    再検証スケジュール  + キャッシュ

キャッシュ状態

状態条件動作
Fresh現在時刻 < 生成時刻 + TTLキャッシュを即座に返す
Stale現在時刻 >= 生成時刻 + TTLキャッシュを返す + バックグラウンドで再検証
Missキャッシュエントリなし生成、キャッシュ、返却

設定

フロントマター

revalidateをフロントマターに追加してISRを有効化:

---
title: マイページ
revalidate: 60
---

# コンテンツはここに

revalidateの値は秒単位です。

TTLガイドライン

コンテンツタイプ推奨TTL
ホット (高トラフィック)300-600秒ホームページ、人気記事
ウォーム (中程度)60-300秒チュートリアル、APIドキュメント
コールド (低トラフィック)30-60秒アーカイブ、古い記事
リアルタイム0常に再生成

ビルド出力

revalidateが設定されたページがある場合、Sol SSGはISRマニフェストを生成:

dist/
└── _luna/
    └── isr.json

マニフェスト形式

{
  "version": 1,
  "pages": {
    "/blog/": {
      "revalidate": 300,
      "renderer": "markdown",
      "source": "blog/index.md"
    },
    "/blog/post-1/": {
      "revalidate": 120,
      "renderer": "markdown",
      "source": "blog/post-1.md"
    }
  }
}

ランタイム動作

Solサーバー統合

SolはISRマニフェストを自動的にロードしてキャッシングを処理:

// ISRハンドラーを初期化
let handler = init_isr(dist_dir)

// リクエストを処理
let (html, needs_revalidation) = handler.handle(path)

if needs_revalidation {
  schedule_revalidation(path)
}

キャッシュキー形式

キャッシュキーにはパスとクエリパラメータが含まれます:

isr:/blog/                     # シンプルなパス
isr:/search/?q=luna&sort=date  # ソートされたクエリパラメータ付き

クエリパラメータは一貫したキャッシュキーのためにアルファベット順にソートされます。

パフォーマンス

ISRは高スループットに最適化されています:

操作スループットレイテンシ
キャッシュ読み取り~6.7M ops/sec0.15μs
キャッシュ書き込み~2.7M ops/sec0.36μs
ステータス確認~4.4M ops/sec0.23μs
フルハンドル~2.4M req/sec0.41μs

キャッシュオーバーヘッドはネットワークI/Oと比較して無視できるレベルです。

例: 階層化TTLのブログ

docs/
├── blog/
│   ├── index.md           # revalidate: 300 (ホット)
│   ├── post-1.md          # revalidate: 120 (ウォーム)
│   ├── post-2.md          # revalidate: 120 (ウォーム)
│   └── archive/
│       ├── index.md       # revalidate: 60 (コールド)
│       └── old-post.md    # revalidate: 60 (コールド)

トラフィックパターンシミュレーション

80/20ルールに従う50,000ページの場合:

階層ページ数TTLトラフィック割合
ホット100300秒80%
ウォーム900120秒15%
コールド49,00060秒5%

ホットページは頻繁なアクセスにより常に新鮮。コールドページは一時的に古いコンテンツを配信後、再検証。

Stale-While-Revalidateパターン

ISRはSWRパターンを実装:

  1. ユーザーA が古いページをリクエスト → 古いコンテンツを即座に取得(待ち時間なし)

  2. バックグラウンド で再生成開始

  3. ユーザーB が同じページをリクエスト → 新しいコンテンツを取得

これにより、ユーザーは再生成を待つ必要がありません。

デプロイ考慮事項

Cloudflare Workers

ISRはCloudflareのエッジキャッシングと連携:

{
  "deploy": "cloudflare"
}

ISRハンドラーはバックグラウンド再検証のためにwaitUntilと統合されます。

メモリキャッシュ

開発およびシングルインスタンスデプロイ用:

let cache = MemoryCache::new()

注: メモリキャッシュは再起動時に失われます。本番環境では分散キャッシュを使用してください。

APIリファレンス

フロントマターオプション

オプションデフォルト説明
revalidateIntNoneTTL(秒単位、0 = 常にstale)

ISRHandler

// ハンドラーを作成
fn ISRHandler::new(cache, manifest, dist_dir) -> ISRHandler

// リクエストを処理 - (html?, needs_revalidation)を返す
fn handle(path: String) -> (String?, Bool)

// 再検証後にキャッシュを更新
fn update_cache(path: String, html: String) -> Unit

CacheEntry

struct CacheEntry {
  html: String         // キャッシュされたHTMLコンテンツ
  generated_at: Int64  // Unixタイムスタンプ(ミリ秒)
  revalidate: Int      // TTL(秒単位)
}

CacheStatus

enum CacheStatus {
  Fresh  // TTL内
  Stale  // TTL超過だがコンテンツあり
  Miss   // キャッシュされたコンテンツなし
}

ベストプラクティス

  1. 適切なTTLを使用 - TTLをコンテンツ更新頻度に合わせる

  2. キャッシュヒット率を監視 - 実際の使用状況に基づいてTTLを調整

  3. エラーを適切に処理 - 再生成失敗時は古いコンテンツを配信

  4. トラフィックパターンを考慮 - ホットページは長いTTLから最も恩恵を受ける

  5. 現実的なデータでテスト - 本番トラフィックパターンをシミュレート

制限事項

  • メモリキャッシュは再起動後に永続化されない

  • 再検証にはサーバーサイド実行が必要

  • クエリパラメータのバリエーションは別々のキャッシュエントリを作成

  • パスマッチングは大文字小文字を区別し、完全一致(末尾スラッシュが重要)