Web Locks APIは、同じサイトを開いた複数のタブやWorkerが同じ処理を同時に走らせないよう、ブラウザ側で順番を付けるためのAPIです。navigator.locks.request() に名前とコールバックを渡すと、その名前のロックを持てたコンテキストだけがコールバックの中を実行し、ほかは終わるまで待ちます。この記事では構文と4つのオプション、例外が投げられる条件、ロック状態の確認方法、ブラウザ対応、そして使うべきでない場面までを、W3Cの仕様書(Editor’s Draft、2025年9月24日版)とMDNの記述に沿って整理します。
まとめ:Web Locks APIの要点
navigator.locks.request(名前, コールバック)で同一オリジンのタブ・Worker間の処理を直列化できる。コールバックが終了した時点でロックは自動的に解放される。- オプションは
mode・ifAvailable・steal・signalの4つ。待たせたくないときはifAvailable、待ち時間に上限を付けたいときはsignalを使う。 - ロックが共有される範囲はstorage bucket単位で、ブラウザの別プロファイルやプライベートウィンドウとは共有されない。サーバー側の排他制御の代わりにはならない。
- Chrome 69・Firefox 96・Safari 15.4以降が対応済みで、MDNのBaselineは「広く利用可能」。secure context(HTTPSやlocalhostなど。埋め込み先の条件にも依存)でのみ
navigator.locksが存在する。
Web Locks APIが解決する競合とロックの有効範囲
JavaScriptは1つの実行コンテキストの中ではシングルスレッドで動きますが、同じアプリを2つのタブで開けば、それは2つの独立した実行コンテキストです。アクセストークンの更新、IndexedDBとサーバーの同期、ファイルのアップロード再開といった処理は、どのタブでも「自分がやるべき仕事」に見えるため同時に走り出します。リフレッシュトークンの再利用検知を持つ認可サーバーであれば、この二重実行だけでセッションが強制的に無効化されることもあります。
Web Locks APIは、この調整を自前のフラグ管理ではなくブラウザに任せる仕組みです。仕様書はロック名について「A resource name is a JavaScript string chosen by the web application to represent an abstract resource.」と定義しており、名前そのものに意味はなく、同じ文字列を要求したコンテキスト同士が順番を取り合うだけの識別子として扱われます。「ネットワークとIndexedDBの同期を1タブだけに担当させる」という使い方は、MDNがリーダー選出パターンとして挙げている代表例です。
有効範囲の理解が実装の分かれ目になります。仕様書は「Pages and workers (agents) sharing a storage bucket opened in the same user agent share a lock manager even if they are in unrelated browsing contexts.」と述べており、ロックマネージャーはstorage bucketごとに1つです。同時に「Separate user profiles within a browser are considered separate user agents. Every private mode browsing session is considered a separate user agent.」とも定義されているため、通常のウィンドウとシークレットウィンドウの間、Chromeのプロファイルが違う2つのウィンドウの間では、同じロック名を使ってもまったく調整されません。「同一オリジンなら必ず効く」と考えて設計すると、この2ケースで二重実行が起きます。
名前の付け方にも制約が1つあります。ハイフン(U+002D)で始まる名前は仕様で予約されており、要求すると NotSupportedError になります。IndexedDBのストア単位でロックを分けたいときの命名例として、仕様書は encodeURIComponent(db_name) と encodeURIComponent(store_name) をスラッシュで連結する方法を挙げています。ブラウザ側のデータ保存先の違いはCookie・localStorage・IndexedDBの違いと使い分け|容量・保存期間・セキュリティ・デメリット比較で整理しています。
基本形とロックの自動解放
request() は名前とコールバックを受け取り、ロックを取得できた時点でコールバックを呼びます。コールバックが非同期関数なら、返すPromiseが決着するまでロックを保持します。以下の例の refreshAccessToken() などはアプリ固有の処理で、実行するには別途実装が必要です。
await navigator.locks.request('token-refresh', async (lock) => {
// ここを実行できるのは、同じ名前のロックを取れた1コンテキストだけ
await refreshAccessToken();
});
// ここに来た時点でロックは解放済み
解放のために明示的なメソッドを呼ぶ必要はありません。コールバックが例外を投げた場合もロックは解放されます。try/finally でロックを戻す、といった後始末のコードは不要です。
released promiseとwaiting promiseの区別
仕様書はロックのライフサイクルに2つのPromiseがあると定義しています。コールバックが返すPromiseがwaiting promiseで、これが決着した時点でロックが解放されます。request() 自体が返すPromiseがreleased promiseで、ロックが解放されたか要求が中断されたときに決着します。
const released = navigator.locks.request('resource', (lock) => {
const waiting = new Promise((resolve) => {
// 好きなタイミングで resolve するまでロックを保持し続けられる
finishLater(resolve);
});
return waiting;
});
await released; // ロックが解放されるまで待つ
この分離があるため、「タブが開いている間ずっとロックを持ち続ける」主タブ方式も仕様の想定内です。仕様書は主タブがロックを保持したまま解放せず、そのタブがクラッシュするか閉じられたときに待機中の別タブが取得して新しい主タブになる、という設計例を挙げています。返り値の扱いも単純で、コールバックの戻り値はそのまま request() のPromiseに伝わります。Promiseの合成やエラー伝播の基本はJavaScriptの非同期エラーハンドリングと並列実行|try/catch・Promise.all・allSettleで扱っています。
4つのオプションと例外が投げられる条件
request() の第2引数には次の4つを渡せます。既定値は仕様書のIDL定義そのままです。
| オプション | 型 | 既定値 | 取れなかったときの挙動 |
|---|---|---|---|
| mode | exclusive / shared |
exclusive |
取得できるまで待機 |
| ifAvailable | boolean | false | コールバックに null を渡して即座に呼ぶ |
| steal | boolean | false | 既存の保持者を解放させて割り込む |
| signal | AbortSignal | なし | 取得待ちの中断時に signal.reason でreject |
mode:読み取りだけなら共有ロック
既定の exclusive は同時に1つのコンテキストしか保持できません。shared を指定したロックは複数のコンテキストが同時に保持できますが、その間は誰も exclusive を取れません。読み取り側を shared、書き込み側を exclusive にすると、データベースの共有ロック・排他ロックと同じ読み書き制御になります。
await navigator.locks.request('settings', { mode: 'shared' }, async () => {
// 他のコンテキストも同時に shared で保持できるが、
// この間 exclusive のロックは誰も取得できない
const settings = await readSettings();
applySettings(settings);
});
ifAvailable:取得できない場合の処理スキップ
先に誰かが処理しているなら自分はやらなくてよい、という場面では待つ必要がありません。ifAvailable: true を指定すると、すぐに取得できない場合はコールバックが null を受け取ります。仕様書はこの挙動について「If the lock cannot be granted, the callback is invoked with null. (Since this is expected, the request is not rejected.)」と明記しており、取得失敗はエラーではありません。
await navigator.locks.request('token-refresh', { ifAvailable: true }, async (lock) => {
if (!lock) {
// 別タブが更新中。自分は何もしないで抜ける
return;
}
await refreshAccessToken();
});
この形を使えるのは、更新の完了を自分のタブで待たなくてよい場合です。更新後のトークンでリトライする場合は、更新完了と新しいトークンの共有を確認する必要があります。ただし仕様書は「Note that this is still not synchronous; in many user agents this will require cross-process communication to see if the lock can be granted.」と注意しており、同期的な tryLock() ではない点は変わりません。ifAvailable を使っても結果は非同期で返るため、完了を扱うには await または then()・catch() を使います。
signal:ロック取得待ちの打ち切り
取得待ちを打ち切りたいときは AbortSignal を渡します。取得前にabortされると、request() のPromiseが signal.reason でrejectされます。引数なしの controller.abort() では AbortError になります。
const controller = new AbortController();
setTimeout(() => controller.abort(), 200);
try {
await navigator.locks.request('resource', { signal: controller.signal }, async () => {
await doWork();
});
} catch (e) {
if (e !== controller.signal.reason) {
throw e; // doWork() が投げた例外は握りつぶさずそのまま伝える
}
// 200ms 以内にロックを取得できなかった
}
ここで押さえておきたいのは、仕様書が「Once the lock has been granted, the signal is ignored.」と書いている点です。signal が効くのは取得待ちの打ち切りだけで、コールバックに入った後の処理を中断する機能はありません。処理そのものにタイムアウトを掛けたいなら、同じ AbortSignal をコールバック内の fetch() などへ別途渡す必要があります。「タイムアウト付きのロック」として設計すると、実行中の処理が想定より長引いたときに止まりません。
steal:ロックの強制奪取と処理競合のリスク
steal: true は、保持中のロックを強制的に解放させて割り込みます。奪われた側の request() が返したPromiseは AbortError でrejectされますが、そのコールバックの中で走っていたコードは止まりません。仕様書も「code previously holding a lock will now be executing without guarantees that it is the sole context with access to the resource」と警告しています。
// 主タブが応答しないと判断したときの復帰処理に限って使う
await navigator.locks.request('primary-tab', { steal: true }, async () => {
await takeOverPrimaryRole();
});
用途は仕様書が挙げるとおり、Service Workerなどの調停役が「ロックを持つタブがもう応答していない」と判断した場合の復帰処理に限られます。通常の排他制御に使うオプションではありません。
NotSupportedErrorになる4つの条件
仕様書の request() のアルゴリズムは、次の4つのいずれかに当たると NotSupportedError でrejectされたPromiseを返すと定めています。
- ロック名がハイフン(U+002D)で始まる
stealとifAvailableが両方truestealがtrueでmodeが"exclusive"以外signalを指定したうえでstealかifAvailableのどちらかがtrue
3番目はMDNのリファレンスには載っていない条件です。奪取する側は排他ロックを要求する必要がありますが、奪われる側の共有ロックも解放対象になります。このほか、ドキュメントがfully activeでなければ InvalidStateError、ロックマネージャーを取得できない環境では SecurityError になります。
query() は現時点のロック状態のスナップショットを返します。返るのは held と pending の2配列を持つプレーンなオブジェクトで、各要素は name・mode・clientId を持ちます。
const state = await navigator.locks.query();
for (const lock of state.held) {
console.log(`保持中: ${lock.name} (${lock.mode}) client=${lock.clientId}`);
}
for (const request of state.pending) {
console.log(`待機中: ${request.name} (${request.mode})`);
}
clientId はコンテキスト(フレームまたはWorker)ごとに一意な文字列で、Service Workers仕様の Client の id と同じ値です。どのタブがロックを握ったままなのかを特定するときの手がかりになります。用途はログとデバッグに限ると考えてください。仕様書自身が「for logging or debugging purposes」と位置づけているとおり、query() の結果を見てから request() するコードは、その2つの操作の間に状態が変わるため排他になりません。
実装パターン:二重実行の抑止とデッドロック回避
排他ロックによる同期処理の直列化
複数タブが同じデータをサーバーと同期する処理では、ロックを取ってから「まだ同期が必要か」を再確認する順序が重要です。ロックを待っている間に、先行したタブが処理を終えている可能性があるためです。
async function syncOnce() {
await navigator.locks.request('db-sync', async () => {
// ロック取得後に再判定する。待っている間に別タブが終えているかもしれない
if (!(await needsSync())) {
return;
}
await syncToServer();
});
}
ifAvailableによるバックグラウンド処理の重複抑止
同期の完了を自分のタブで待つ必要がないなら、ifAvailable: true に切り替えるだけで待機がなくなります。UIをブロックしないバックグラウンド同期では、この形が扱いやすくなります。判断基準は単純で、後続処理がその結果を必要とするなら待つ、必要としないなら諦める、の二択です。トークン更新のように「更新後のトークンで即座にリトライしたい」処理は前者、統計情報の送信のように結果を使わない処理は後者になります。
複数ロックの取得順序統一によるデッドロック回避
ロックを入れ子で取得すると、取得順が食い違ったときにデッドロックになります。仕様書の例では、あるスクリプトがAを取ってからB、別のスクリプトがBを取ってからAを要求すると、どちらも進めなくなります。ただしこの停止は該当のコードにとどまり、仕様書は「This will not affect the user agent as a whole, pause the tab, or affect other script in the origin」と説明しています。タブが固まるわけではありません。
予防策として仕様書が示しているのは、複数ロックを常に同じ順序で取得するヘルパーです。名前でソートしてから再帰的に取得すれば、どのコンテキストから呼んでも取得順が一致します。
async function requestMultiple(resources, callback) {
const sorted = [...new Set(resources)].sort();
const acquire = (index) => {
if (index === sorted.length) {
return callback();
}
return navigator.locks.request(sorted[index], () => acquire(index + 1));
};
return acquire(0);
}
// どちらの呼び出しも 'account' -> 'inventory' の順で取得する
await requestMultiple(['inventory', 'account'], async () => { /* ... */ });
await requestMultiple(['account', 'inventory'], async () => { /* ... */ });
そもそも入れ子にしない設計のほうが安全です。データベース側のデッドロックの原因と解消手順はデッドロックとは?データベースで起きる原因・具体例・検出と解消・予防策で解説しています。
ブラウザ対応と動作条件(2026年9月時点)
| ブラウザ | 対応バージョン |
|---|---|
| Chrome | 69 |
| Edge | 79 |
| Firefox | 96 |
| Safari(macOS / iOS) | 15.4 |
MDNのBaselineは「広く利用可能」で、日本語版は「2022年3月以降、すべてのブラウザーで利用可能です。」と記載しています。Safari 15.4のリリースが2022年3月であり、この時点で主要ブラウザが出そろいました。対応ブラウザだけをサポート対象にするならフォールバックは省略できます。旧バージョンを含める場合は、機能検出と非対応時の動作を設計してください。
一方で、実行条件は2つあります。IDLに [SecureContext, Exposed=(Window,Worker)] と書かれているとおり、secure context(HTTPSやlocalhostなど。埋め込み先の条件にも依存)でなければ navigator.locks は存在せず、利用できるのはWindowとWorkerのコンテキストです。HTTPSで配信していても、安全でないページに埋め込まれたiframeの中ではsecure contextになりません。navigator.locks が undefined になる原因は、非対応ブラウザや、安全なコンテキストではない実行環境が考えられます。
if (!('locks' in navigator)) {
// 非対応ブラウザではなく、secure context でない可能性を先に疑う
console.warn('Web Locks API is unavailable in this context');
}
仕様書自体はW3C Web Applications Working Groupが2025年9月24日に公開したWorking Draftで、勧告には至っていません。実装が先行して普及している状態です。Workerからの利用についてはWeb Workerとは?重い処理を別スレッドへ逃がす実装と使い分けを解説もあわせて確認してください。
Web Locks APIを選ぶべきでない場面
このAPIで解決できる範囲は、1つのブラウザプロファイルの中に閉じています。次の4つに当てはまるなら、Web Locks APIは選択肢から外してください。
サーバー側のデータ整合性を守りたい場合は使えません。ロックが共有されるのは同じstorage bucketを持つコンテキスト同士だけです。別の端末、別のブラウザ、シークレットウィンドウ、別プロファイルからのリクエストはまったく調整されません。二重注文や在庫の重複更新を防ぐ目的なら、サーバー側の排他制御や冪等キーが必要です。クライアント側のロックは、あくまでUXと無駄なリクエストの削減のための仕組みです。
Service Workerで長時間ロックを保持する設計は避けてください。Service Workerはイベント処理が終わればアイドル状態で終了させられます。ロックを保持したまま終了すれば当然ロックは失われ、調停役として機能しません。タブ側で主タブを選出する場合も、凍結や破棄への対応が必要です。凍結前のロック解放と、復帰時の再取得を設計してください。
処理そのものにタイムアウトを掛けたい場合、signal では要件を満たしません。前述のとおり、取得後の signal は無視されます。長時間走る可能性のある処理をロック内に置くなら、その処理自身に中断機構を持たせてください。
Wake Lock APIと取り違えないでください。名前が似ていますが、Wake Lock APIは画面スリープを抑止するAPIで、リソースの排他制御とは無関係です。タブ間でデータそのものをやり取りしたいだけなら、BroadcastChannel APIとは|JavaScriptでタブ間通信を実装する方法・ブラウザ対応・React実装例のほうが用途に合います。BroadcastChannelは通知の配信、Web Locksは実行の調停という役割分担で考えると、選び分けを間違えません。
よくある質問
Web Locks APIとは何ですか?
同一オリジンの複数のタブやWorkerが同じリソースを同時に操作しないよう、名前付きのロックで実行順を調整するブラウザAPIです。navigator.locks.request() でロックを要求し、コールバックが終わると自動的に解放されます。
まず、安全なコンテキストで実行しているかを確認してください。IDLに [SecureContext] が指定されているため、通常のHTTP配信では利用できませんが、localhostやループバックアドレスなど、信頼できるオリジンとして扱われる例外があります。HTTPSかlocalhostで動かしているか先に確認してください。Chrome 69・Firefox 96・Safari 15.4以降であれば、ブラウザの対応状況が原因になることはまずありません。
ロックの取得を待たずに処理をスキップできますか?
{ ifAvailable: true } を指定すると、すぐ取得できない場合はコールバックの引数が null になります。これはエラーではないので、if (!lock) return; で抜ける形が基本です。
Wake Lock APIとWeb Locks APIは同じものですか?
別のAPIです。Wake Lock APIは画面がスリープしないよう保持するためのもので、Web Locks APIはリソースへのアクセス順を調整するためのものです。機能も呼び出し方も共通点はありません。
JavaScriptでmutexを自前実装する必要はありますか?
1つのタブの中だけで非同期処理を直列化したいなら、Promiseのチェーンで足ります。タブやWorkerをまたぐ調停が必要な場合に限り、自前実装ではなくWeb Locks APIを使ってください。localStorageの単純なフラグ方式では、クラッシュ後の残留や読み取りと書き込みの間の競合に対処する必要があります。