no-ui-flash について
クライアントレンダリング型アプリの開発者向けデザインスキルです。状態が未確定でも誤解を招かないプレースホルダーを選ぶ方法を、エッジでの確認、ヒント Cookie、タイミングを考慮したテストで説明します。
主な機能
- 01エッジで認証と設定を解決
- 02ヒント Cookie で UI 状態を初期化
- 03状態変化に安全なフォールバック
- 04戻り先を検証して深いリンクを保持
- 05プローブを遅らせて読み込み UI をテスト
活用シーン
- 01未認証ユーザーへのアプリ外枠表示を防止
- 02結果がない検索でスケルトン表示を回避
- 03設定読み込み時のテーマのちらつきを防止
- 04ログインリダイレクト経由の深いリンクをテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rhyssullivan/ skills --skill no-ui-flashインストール後、Claude Code で/no-ui-flashを入力して読み込まれたか確認します。
詳細
間違った UI を一瞬でも表示しない
クライアントでレンダリングするアプリには、最初の描画からクライアント側のデータが取得できるまでの間に、時間差があります。よくある不具合は、その間を一つの結果に賭けたプレースホルダーで埋め、その賭けに負けることです。
- サインアウト状態の訪問者に表示される アプリシェルのスケルトン(サイドバー、ナビゲーション、コンテンツカード)。ログイン画面にリダイレクトされる直前に、たどり着くことのないアプリが表示され、その後、唐突に画面が切り替わります。
- 「検索結果がありません」に落ち着く検索/一覧ページに表示される 結果グリッドのスケルトン。実在しないコンテンツをスケルトンが約束してしまいます。
- 設定が読み込まれた後にダークテーマへ切り替わる ライトテーマでの初回描画。
- 実際には 404 になるルートのスケルトン。
ルールは、期待する状態ではなく、確認済みの状態に合ったプレースホルダーを描画することです。 確認できない場合は、都合のよい結果を前提にせず、どの結果でも正しい中立的でレイアウトが安定したプレースホルダーを描画します。これを実現するには三つのレイヤーがあり、それぞれが次のレイヤーの負担を軽減します。
レイヤー 1 — ドキュメントが配信される前に、エッジで状態を解決する
SPA の HTML を配信するサーバー/エッジは通常、クライアントが往復通信を費やして調べることになる情報を、すでに保持しています。それをそこで活用します。
- 認証:多くのセッション方式では、アップストリームへの往復通信なしで検証できます。シールまたは署名付き Cookie はローカルの暗号処理で検証でき、JWT はキャッシュされた JWKS を使って検証できます。ドキュメントリクエストを制御し、サインアウト状態なら、アプリの HTML が生成される前に
302 /login?returnTo=<path>へリダイレクトします。誤ったシェルは配信されなければ、一瞬でも表示されません。ゲート対象のパスで SPA を受け取るユーザーは、これでサインイン済みだと 確認された ため、クライアントのスケルトンも正しいものになります。 - データに応じて変わる状態:エッジが「空か、データがあるか」を低コストで判定できる場合(件数、KV フラグ、最後に確認した状態を記録する Cookie など)、一種類のスケルトンですべてに対応するドキュメントではなく、正しいバリアント(空状態の HTML、データがある場合のシェル、オンボーディングへのリダイレクトなど)を配信できます。
- 設定(テーマ、ロケール、表示密度):エッジで設定 Cookie を読み取り、初期 HTML で正しいバリアントを配信します。
<html>のクラス指定は、クライアント側での切り替えより確実です。
問題になりやすいエッジケース(認証について説明していますが、Cookie に保持されるあらゆる状態に当てはまります)。
- ドキュメントのナビゲーションのみをゲートします(
sec-fetch-dest: documentを伴う GET/HEAD。これがない場合はAccept: text/htmlを使用します)。API ルート、モジュールリクエスト、ヘルスプローブは、それぞれ自身で応答します。 - 無効な状態を運ぶ Cookie は、迂回するだけでなく消去します。 検証に失敗する Cookie は、存在しない場合より厄介です。Cookie の 存在 を条件にした処理は、削除されるまで誤動作し続けます。レスポンスで有効期限を切らしてください。
- 検証によってローテーションされた情報は永続化します。 検証でトークンが更新された場合、新しい値をこのレスポンスでブラウザーに必ず渡す必要があります。更新トークンは通常、一度しか使えないため、ローテーションを黙って破棄すると、後でセッションが機能しなくなります。これはリダイレクト時だけでなく、通常の配信経路にも当てはまります。
- エッジでのチェック中に障害が発生した場合は、安全な状態(サインアウト、デフォルトのテーマ)にフォールバックし、500 エラーにはしません。
レイヤー 2 — 正しい表示をすぐに行うためのヒント Cookie
クライアントが最初の確認(/me、最初の検索、設定の取得)で通常把握する内容を、HttpOnly ではない Cookie に保存しておけば、次回の読み込み時に待ち時間なく正しい表示を行えます。
- サーバーが状態を確認するたびに、クライアントはこの値を書き込みます。次回の読み込み時には、確認処理の実行中に楽観的な状態を設定するために読み取ります。(認証:ID の表示データ。検索/一覧:「このアカウントにはデータがある」か、直近の結果件数。テーマ:解決済みの設定。)
- これはヒントであり、決して権限の根拠ではありません。実際の認可と実データは引き続きサーバーから取得します。古くなったヒントや偽造されたヒントで変わるのは、短時間表示されるプレースホルダーだけです。ペイロードにはユーザーがすでに把握している表示データのみを含め、読み取り時にスキーマを検証し、不正な形式の値は存在しないものとして扱います。
- 解決済みの確認結果は常に優先されます。結果が届いた時点で必ず整合させます。
- 元の状態が失われるときは、必ずあらゆる場所でヒントを消去します(ログアウト、アカウントの消去、設定のリセット)。該当するレスポンスでサーバー側から消去し、確認結果と矛盾する場合はクライアント側でも消去します。真実でなくなった後も残るヒントは、誤った UI を確信をもって表示してしまいます。
- SSR/ハイドレーションを行うアプリでは、Cookie は最初のレンダー時ではなく、マウント後(effect/state)に読み取ります。最初のクライアントレンダーはサーバーの HTML と一致している必要があります。切り替えによるコストは 1 フレームであり、ラウンドトリップではありません。
レイヤー 1 と 2 を組み合わせると、クライアントの「読み込み中」プレースホルダーが表示されるのは、本当にその表示が正しい状態(たとえば、確認済みユーザーが新しいブラウザーから初めてアクセスした場合)に限られます。これで、ようやく楽観的なスケルトン表示が実態に即したものになります。
レイヤー 3 — クライアント側のフォールバックをセーフティネットにする
アプリ内の状態ゲートは、新規読み込みを処理しなくなります。処理するのはセッション中の状態変化(別タブでのログアウト、期限切れ、別の場所でのデータ削除)です。
- 現在の UI が無効になる状態では、フルナビゲーションで遷移し、その間は中立的な画面(テーマに沿った空白画面)を表示します。ユーザーがたった今失った UI のプレースホルダーは、決して表示してはいけません。
- 不明なルートには、シェルやスケルトンではなく、実際の 404 ページが必要です。
notFoundルートがない場合、存在しない URL に誤ってスケルトンを表示しているのはフォールバックかもしれません。確認してください。
リダイレクトで戻るための情報(returnTo)
エッジでリダイレクトする場合(ログイン、オンボーディング)、ディープリンクは迂回後も維持する必要があります。returnTo 用の Cookie をもう 1 つ追加するのは避けてください。フローが OAuth 形式なら、state パラメーターはすでにプロバイダーを経由してそのまま往復し、CSRF チェック(Cookie に保存した state とコールバック時のタイミングセーフな比較)によって認証されています。この仕組みに乗せましょう。state = base64url(JSON { nonce, returnTo })。値は 1 つ、Cookie も 1 つで、比較を破らずに攻撃者が遷移先を差し替えることはできません。
returnTo がどこから渡されても(ゲートのクエリ、ログインページ、デコード済みのコールバックの state)、同一オリジンの相対パスとして検証してください。/ で始まり、// では始まらず(プロトコル相対 URL は絶対 URL を装ったものです)、API パスでもないことが条件です。それ以外はすべて / にフォールバックします。デコードはどのような入力でも安全に処理できる必要があります。プロバイダーから、発行していない state が含まれるコールバックが届くこともあります。不正な値は例外を投げるのではなく、「returnTo なし」として扱います。
ローディングウィンドウのテスト
このバグはタイミングウィンドウで発生するため、テストではそのウィンドウが開いた状態を維持する必要があります。
- プローブをインターセプトして遅延させます(例:検索エンドポイントまたは
/meに対する Playwright のpage.routeでスリープを設定します)。遅延中に描画された内容をアサートします。リダイレクトされた訪問者の場合はスケルトン要素がゼロの遷移先ページ、ヒントを受け取ったユーザーの場合は実際の UI に加え、描画時点でプローブがまだ完了していなかったことを示すフラグを確認します。 - ワイヤー越しにリダイレクトの往復全体を実行します。保護されたディープリンク → returnTo を含むリダイレクト → プロバイダー → コールバック → ディープリンクに到着、という流れです。次に、偽造したバージョンを試します。オリジン外の returnTo でもフローは完了しますが、到着先は
/になります。 - Set-Cookie ヘッダーで Cookie の衛生状態をアサートします。無効な state キャリアは、そのヒントとともにクリアされます(Max-Age=0)。死亡イベント(ログアウトなど)では、両方とも削除されます。
- 有効期限切れを待たずにローテーション経路を実行します。キャリアがシール済み Cookie の場合、サーバーが使うものと同じライブラリでシールを解除し、内部トークンの署名を破損させてから再度シールします。エッジでは無効だが更新可能な状態として扱われます。ページが配信されること、レスポンスにローテーション後の値が設定されること、使用済みの値が再利用時に拒否されることをアサートします。