複数サイトを横断する「全サイトのプラグイン更新状況をまとめて表示するダッシュボード」を v1.6.2 で実装したとき、ある UX の壁にぶつかりました。ダッシュボードを開くだけで毎回 24.5 秒待たされる。サイト数が増えるほどこの待ち時間は伸びていきます。
ユーザーから「心の準備ができていないのに、開いただけで重い処理が走る」という指摘がありました。今回はこの課題に対して採用した、ちょっと特殊な非同期 UX の組合せ — キャッシュ即表示・明示 fetch・閉じても進む notice の 3 点 — をまとめておきます。
何が時間を食っていたのか
内部で起きていたのは、複数サイトに対する並列 SSH スキャンです。各サイトに wp plugin list --update=available を投げて、要更新プラグインを集計する。WordPress 保守ツールとしては自然な動作ですが、毎回 fetch すると 1 回あたり数十秒の沈黙が発生します。
補足: SSH(Secure Shell の略・サーバーに安全にログインしてコマンドを実行するための仕組み)越しに WP-CLI(WordPress 公式のコマンドラインツール)を叩いている部分です。並列化はしているものの、ネットワークレイテンシとサーバー側応答の合計が読み込み待ちとして体感されます。
「リアルタイム性が必要なのか?」を問い直したのが転機でした。ユーザーは大半の場合、今この瞬間の正確な更新情報ではなく、直近数日の俯瞰を見たいだけ。であれば、ダッシュボードを「リアルタイムモニター」ではなく「棚卸し・実行計画の支援ツール」として再定義してよさそうだ、というのが基本方針になりました。
要素 1 — キャッシュ即表示(localStorage + 7 日 TTL)
各サイトのスキャン結果をブラウザの localStorage に保存し、次回オープン時はそれを即時表示する設計に変えました。TTL(time-to-live・キャッシュ有効期間)は最初 24 時間で検討しましたが、最終的に 7 日に伸ばしています。
const _DASH_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
// 保存
localStorage.setItem(_DASH_CACHE_KEY, JSON.stringify(payload));
// 復元
const raw = localStorage.getItem(_DASH_CACHE_KEY);
if (raw) {
const cached = JSON.parse(raw);
if (Date.now() - cached.timestamp < _DASH_CACHE_MAX_AGE_MS) {
renderDashboard(cached.data); // 0 秒表示
}
}
7 日という長めの TTL を選んだのは、メンテナンス実行のフックで自動的にキャッシュを無効化する仕組みが別途あるためです。実際にプラグインを更新したサイトは次のメンテナンスで自動的に最新化されるので、「メンテ対象は常に新鮮・メンテ対象外は最長 7 日古くてもよい」というメンタルモデルが成立します。
要素 2 — 明示 fetch(自動 fetch の廃止)
キャッシュがある場合は 0 秒表示で問題が解決します。問題は キャッシュが無い場合(初回・期限切れ・手動クリア直後)に何を出すか。
ここで自動 fetch を始めてしまうと、結局「開いた瞬間に勝手に重い処理が始まる」という元の問題が再発します。採用したのは 「明示的にユーザーが取得を開始する」設計です。キャッシュ無しのときは、中央にこんなプロンプトを出します。
📊
プラグイン更新情報を取得しますか?
(複数サイトへ並列 SSH スキャンを実行します・数十秒かかる場合があります)
[ 📊 取得を開始 ]
ボタンを押した瞬間に並列スキャンが走ります。ユーザーは「これから重い処理が始まる」と知った上でスタートできる。同じ 24.5 秒でも、心の準備ができている 24.5 秒と、何が起きるか分からない 24.5 秒では、体感がまるで違います。
要素 3 — 閉じても進む notice
3 つ目はもっと地味ですが効きます。スキャン中にユーザーがモーダルを閉じたい場面 — 他の作業をしたい・別画面を見たい — に対して、「閉じても処理は継続する」設計です。スキャン進行中はモーダル下部にこの notice を出します。
この画面を閉じても処理は進みます。完了後にもう一度開けば結果が表示されます。
実装上は、fetch を Promise で起動して resolve 時に localStorage へ書き込む — ただそれだけ。モーダルの開閉とは独立しています。閉じてから 30 秒後に再度開くと、キャッシュ即表示パスに乗って 0 秒で結果が表示されます。
この notice 文言は初期は 2 行で、💡 アイコン付きで出していました。リリース後のフィードバックでアイコンを削除して 1 行に圧縮 — 短い修正でも体感はけっこう変わります。
キャッシュ無効化のフックを残す
キャッシュ即表示の弱点は 「古い情報を表示し続けるリスク」です。これは「メンテナンス実行成功時にダッシュボードキャッシュを破棄する」フックで補っています。
// メンテナンス完了後の処理
if (maintenance_result.success) {
localStorage.removeItem(_DASH_CACHE_KEY);
}
メンテを実行したサイトは確実に状態が変わっているので、次にダッシュボードを開いた時は明示 fetch プロンプトが出る。「ユーザーが何もしていない時は古い情報でよい・状態が変わった時は明示 fetch を促す」という整理になります。
まとめ — 非同期 UX の 3 要素
待ち時間のかかる処理を画面に組み込むときに、この設計から取り出せる一般化できそうな原則は次の 3 つでした。
- 即時表示の経路を必ず用意する — キャッシュでも古いデータでも、「何かを表示する」だけで「沈黙」を消せる
- 重い処理は明示同意で起動する — ユーザーに「今から重い処理が始まる」と伝えてからスタートする
- 閉じても継続する・再開できる — 待ち時間中の画面拘束を外す。閉じた後に戻ったら結果が見えている、を成立させる
「リアルタイムモニター」を作っていたつもりが「棚卸しツール」として再定義することで、3 要素全てが噛み合う設計に着地できました。多サイト・多レコードを扱う管理画面では、似たような UX 設計の引き出しを持っておくと、24.5 秒の沈黙を消すヒントになるかもしれません。