Web Locks APIでマルチタブの二重実行を防ぐ|リーダー選出の実装

同期するのは1タブだけ:Web Locks APIで同一オリジンのタブを制御 Web API・マルチタブ制御
カテゴリー
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秒ごとに必ず動く」という保証にはなりません。

動作確認

  1. 同一originを3タブで開き、リーダーが1つだけになる
  2. リーダータブを閉じ、別タブが引き継ぐ
  3. 再読込、クラッシュ相当、スリープ復帰を確認する
  4. 別originではロックが共有されないことを理解する
  5. 同期APIが重複要求を安全に扱う
  6. Web Locks非対応時の方針を表示する

DevToolsから確認する場合はnavigator.locks.query()で保持中・待機中のスナップショットを取得できますが、結果直後に状態が変わる可能性があります。

トラブル対処

  • 全タブがフォロワー:再試行loopと全タブのロック名を確認します。
  • 引継ぎが遅い:バックグラウンドタイマーの間引きを考慮し、待機型要求も検討します。
  • 別端末で二重実行:サーバー側のidempotency keyやunique制約を追加します。

まとめ

Web Locks APIを使うと、複数タブのうち1つだけへ仕事を任せられます。ロック中のコールバックを適切に保持・解放し、引継ぎ、非対応環境、サーバー側の重複防止まで設計します。

参考リンク

タイトルとURLをコピーしました