- カテゴリー
- Web API・マルチタブ制御
- 公開日
- 2026.09.10
はじめに
同じWebアプリを3タブ開くと、定期同期、通知取得、IndexedDBの整理が3回ずつ走ることがあります。サーバー負荷だけでなく、同じデータへの競合も起きます。
Web Locks APIは、同一origin内のタブやWorkerが名前付きロックを非同期に取得するAPIです。この記事では1タブだけをリーダーにし、閉じたら別タブが引き継ぐ構成を作ります。
仕組み
ロックはnavigator.locks.request()のコールバックが完了するまで保持されます。リーダーは完了しないPromiseを待ち、ページ終了時にそのPromiseを解決してロックを返します。
localStorageへ時刻を書いてリーダーを決める方法は、読取と書込が原子的ではなく競合しやすいため、排他処理には専用APIを使います。
完成コード
const channel = new BroadcastChannel("app-leader-status");
const status = document.querySelector("#status");
let stopped = false;
let releaseLeader = null;
let syncTimer = null;
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function startLeaderWork() {
status.textContent = "このタブがリーダーです";
channel.postMessage({ type: "leader-active" });
syncTimer = setInterval(() => {
channel.postMessage({ type: "heartbeat", at: Date.now() });
console.log("定期同期を実行");
}, 5000);
}
function stopLeaderWork() {
clearInterval(syncTimer);
syncTimer = null;
status.textContent = "フォロワーです";
}
async function electionLoop() {
if (!navigator.locks) {
status.textContent = "Web Locks APIを利用できません";
return;
}
while (!stopped) {
let becameLeader = false;
await navigator.locks.request(
"background-sync-leader",
{ ifAvailable: true },
async (lock) => {
if (!lock) return;
becameLeader = true;
startLeaderWork();
await new Promise((resolve) => {
releaseLeader = resolve;
});
releaseLeader = null;
stopLeaderWork();
},
);
if (!stopped && !becameLeader) await delay(1500);
}
}
channel.addEventListener("message", (event) => {
if (event.data?.type === "heartbeat" && !syncTimer) {
status.textContent = "別タブがリーダーです";
}
});
addEventListener("pagehide", () => {
stopped = true;
releaseLeader?.();
channel.close();
});
electionLoop().catch((error) => {
status.textContent = `リーダー選出に失敗: ${String(error)}`;
});
ifAvailableを使う理由
通常のexclusive lock要求は、先行ロックが解放されるまで待機列に入ります。ifAvailable: trueなら、取得できないときコールバックへnullが渡り、UIをフォロワーとして動かせます。例では1.5秒後に再試行します。
厳密な即時引継ぎが必要なら待機型ロックも選択肢ですが、タブ数だけ待機要求が積まれます。アプリの停止条件とキャンセル方法を先に決めます。
ロック名の設計
ロック名は同じorigin内で共有されます。ユーザーやワークスペースごとに仕事が別なら、衝突しない安定した識別子を付けます。ただしメールアドレスやtokenなどの秘密をロック名へ含めません。
const lockName = `sync:${workspaceId}`;
ロックが保証しないこと
Web Locksは同一originの協調するブラウザ文脈を調停します。別ブラウザ、別端末、別origin、APIを使わないコードまでは排他しません。サーバー側の重複防止にはidempotency key、DB transaction、unique制約などを併用します。
また、タブがバックグラウンドに入るとタイマーが間引かれます。「リーダーだから5秒ごとに必ず動く」という保証にはなりません。
動作確認
- 同一originを3タブで開き、リーダーが1つだけになる
- リーダータブを閉じ、別タブが引き継ぐ
- 再読込、クラッシュ相当、スリープ復帰を確認する
- 別originではロックが共有されないことを理解する
- 同期APIが重複要求を安全に扱う
- Web Locks非対応時の方針を表示する
DevToolsから確認する場合はnavigator.locks.query()で保持中・待機中のスナップショットを取得できますが、結果直後に状態が変わる可能性があります。
トラブル対処
- 全タブがフォロワー:再試行loopと全タブのロック名を確認します。
- 引継ぎが遅い:バックグラウンドタイマーの間引きを考慮し、待機型要求も検討します。
- 別端末で二重実行:サーバー側のidempotency keyやunique制約を追加します。
まとめ
Web Locks APIを使うと、複数タブのうち1つだけへ仕事を任せられます。ロック中のコールバックを適切に保持・解放し、引継ぎ、非対応環境、サーバー側の重複防止まで設計します。

