Relayer
Docs機能defer

Defer コンポーネント#

ログイン名・カート個数・A/B バケットなど「ユーザーごとに違うが、それ以外はキャッシュに乗せたい」部分は、コンポーネントを 2 つに分割します。

  1. ベースコンポーネント — 実際の描画担当。インラインでも再利用できる。
  2. defer ラッパー — Defer 設定を持つだけのラッパー。

ページはラッパーを参照し、SSR ではフォールバックだけが埋め込まれます。本体はページ読み込み後に専用エンドポイント GET /_defer/{name} で取得され、プレースホルダがその場で置き換わります。メイン HTML はユーザー状態に依存しないので CDN エッジでキャッシュできます。

<?php
// src/Components/UserHeader.psx — 本体(インラインでも再利用可)
use Polidog\Relayer\Auth\SessionStorage;

return function (array $props, SessionStorage $session) {
    $user = $session->get('user');
    $name = \is_array($user) && isset($user['name'])
        ? (string) $user['name']
        : 'ゲスト';

    return <header>こんにちは {$name} さん</header>;
};
<?php
// src/Components/UserHeaderDeferred.psx — defer ラッパー
use Polidog\Rosanna\Defer\Defer;
use function Polidog\Rosanna\Defer\deferred;

return deferred(
    fn (array $props) => <UserHeader />,
    new Defer(name: 'user-header', cacheControl: 'private, no-store'),
);
{/* ページ側 — fallback は普通の prop として渡す */}
<UserHeaderDeferred fallback={<HeaderSkeleton />} />

手動の登録・ルート設定は不要#

defer エンドポイントを使うために、ルートを定義したり DI コンテナに登録したりマニフェストを手で編集したりする作業は一切不要です。 vendor/bin/relayer compile(または vendor/bin/rosanna-defer compile src/Pages)が Defer 設定を自動で発見し、 deferred-manifest.php を書き出します。ルーターは GET /_defer/{name} をページ/レイアウト処理より前に RosannaDefer::handleDeferred() へ振り分けるので、ラッパーを書くだけでエンドポイントが立ち上がります。エンドポイントは登録時の Cache-Control を返すため、共通のお知らせバーは public, s-maxage=60、セッション依存の UserHeader は private, no-store と、コンポーネント単位で キャッシュ戦略を決められます。

セッション値が必要な defer コンポーネントでは $_SESSION を直接読まず、 Polidog\Relayer\Auth\SessionStorage を型指定して受け取ります。既定実装の NativeSession は最初の get() / set() まで session_start() しないため、ページ本体の SSR ではセッション Cookie を出さず、/_defer/... のサブリクエスト側だけがユーザー状態を読みます。この例のようなユーザー別フラグメントは cacheControl: 'private, no-store' にして、CDN やブラウザの共有キャッシュに載せないでください。

親 → 子へ値を渡す#

fallback 以外の prop は自動でクエリ文字列になります。

<PostCommentsDeferred fallback={<Skeleton />} post_id={$postId} sort="new" />

これは GET /_defer/post-comments?post_id=123&sort=new になり、ラッパー内側のクロージャでは $props['post_id'] で受け取れます。 スカラ(int / string / float / bool)のみで、URL 経由のため値は文字列化されます。配列・Element・Closure は渡せません。

クライアントキャッシュ#

rosanna-defer.js は defer 取得の前に 2 段のキャッシュを置きます。

  • L1 — インメモリ。 ページ生存期間中のみ。常に有効で、フルリロードで消えます。
  • L2 — localStorage。 リロード/タブをまたぐ。完全に opt-inで、 Defer::$localCache = true を指定したコンポーネントだけ保存します(rosanna-defer.js は HTTP の Cache-Control を見ません)。共有端末で前ユーザーの defer 内容が漏れないための設計です。フォームや type=hidden の入力を含む断片は、localCache を指定しても保存しません(CSRF トークンを端末に残さないため)。
deferred($render, new Defer(name: 'announcement-bar', localCache: true));

既定では時間失効しません。無効化は DEFER_CACHE_VERSION のバンプ(デプロイ時)か clearDeferCache()(実行時)で行います。経過時間で失効させたい場合は localCacheTtl(秒)を足します(localCache: true 必須。正の TTL を単独指定すると例外)。

new Defer(name: 'feed', localCache: true, localCacheTtl: 60)
window.Rosanna.clearDeferCache();                // 両層を全消去
window.Rosanna.clearDeferCache('post-comments'); // 特定 defer 名

明示的リロード#

既定では defer フラグメントは一度だけ取得され、ラッパーごと消えます。Defer::$reloadable を opt-in すると、再取得可能なラッパーを DOM に残せます(一覧の再取得などに)。

new Defer(name: 'todo-list', reloadable: true)
window.Rosanna.reloadDefer('todo-list'); // 命令的に再取得
{/* 任意要素のクリックで再取得 */}
<button data-rosanna-reload-defer="todo-list">更新</button>

リロードは毎回その URL の両キャッシュ層を先に破棄するので、常に最新のサーバー状態を反映します。

制約#

  • モードごとに別コンポーネント。 インライン=ベース、defer= Defer 付きラッパー。同一コンポーネントのコールサイト切替は非対応。
  • defer 名は URL-safe([A-Za-z0-9_-]+)かつアプリ内で一意。 重複するとコンパイル時エラー。
  • params はスカラのみ。 クエリ文字列経由のため。
  • 認可はコンポーネント側の責務。 名前と params は URL に露出するので、機微なデータを返すなら自分でセッション/権限を確認する。
  • セッションを使う応答は共有キャッシュに載らない。 公開の cacheControl を指定していても、Cookie や署名を含む応答は private, no-store になります。
  • 入れ子の defer も動作。 解決後の出力にさらに defer があれば再帰的に hydrate されます。
  • JS なしのユーザーは fallback しか見えません。 仕様上のトレードオフ。

このドキュメントサイト自体は Defer コンポーネントを使っていません(サーバーレンダリングのみ)。なお $ctx->js($src, defer: true) は <script defer> 属性のことで、ここでいう Defer コンポーネントとは別物です(→「ページ単位のスクリプト」)。

最終更新 — 2026-11-04
改訂履歴 (5)
  • v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
  • v0.31.0: defer 内の useState、共有キャッシュの扱いを追記
  • defer の session サンプルを SessionStorage 注入に更新
  • 「配線は不要」を「手動の登録・ルート設定は不要」に。直訳の配線(wiring)が不明瞭なため何が不要か具体化
  • 新規作成