Web Locks APIとは何か?複数タブやワーカー間でリソースを同期する新APIの概要と役割を解説
目次
- 1 Web Locks APIとは何か?複数タブやワーカー間でリソースを同期する新APIの概要と役割を解説
- 2 Web Locks APIの特徴:排他ロックと共有ロックなどユニークな機能と動作特性について詳しく解説
- 3 Web Locks APIの使い方:LockManagerを用いた基本的な非同期ロック取得の手順を解説
- 4 Web Locks APIの基本的な使い方と実装例:シンプルなコードで見るロック取得と解放の方法を解説
- 5 Web Locks APIの利用シーン・ユースケース:複数タブでのデータ同期から共同編集まで幅広く紹介
- 6 Web Locks APIのメリットとデメリット:活用する利点と注意すべき欠点をそれぞれ詳しく解説する
- 7 Web Locks APIの対応ブラウザと利用上の注意点:サポート状況と導入前に知っておきたい事項を解説
- 8 Web Locks APIの具体的なサンプルコード集:高度な機能を使った実践例コードを紹介して解説する
- 9 Web Locks API利用で遭遇しがちなエラーとその対策:事例から学ぶトラブルシューティング方法
- 10 他の排他制御手法との比較:Web Locks APIと従来のブラウザ上のロック機構との違いを徹底比較
Web Locks APIとは何か?複数タブやワーカー間でリソースを同期する新APIの概要と役割を解説
Web Locks APIは、ウェブアプリケーション内で共有リソースへのアクセスを排他的または共有的に制御できる新しいブラウザAPIです。例えば同一サイトを複数のタブで開いている場合でも、Web Locks APIを利用すれば特定の「ロック名」を取得したタブだけがリソースにアクセスし、他のタブは処理が終わるまで待機するといった同期処理を実現できます。これにより、同じオリジン内で並行して動作するスクリプト間での競合状態(いわゆるレースコンディション)を防ぎ、データの整合性を保つことができます。
従来、ブラウザ上ではこうした排他制御の仕組みが提供されておらず、開発者は工夫して問題を回避する必要がありました。しかしWeb Locks APIの登場により、ブラウザ間や複数スレッド間でのリソース共有問題に対して標準化された解決策が得られました。本節では、このAPIが生まれた背景や目的、基本概念について解説します。
Web Locks API誕生の背景と目的:ブラウザ上でロック機構が求められた背景とその目的を徹底解説
JavaScriptは本来シングルスレッドで動作し、単一のページ内では同時に複数の処理が競合することはありません。しかし近年、Web WorkerやService Workerといったマルチスレッド機構の導入により、ウェブアプリでも実質的な並行処理が可能になりました。さらにユーザーが同じアプリケーションを複数のタブで開くことも一般的です。こうした状況では、複数の実行環境が同時に同じリソース(例えばデータベースやネットワーク通信)にアクセスし、競合や不整合が生じる可能性があります。
従来は、ブラウザ上でこうした競合を防ぐための標準的な手段がありませんでした。開発者はローカルストレージを使ったフラグ管理や、サーバー側でシリアライズするなどの工夫でしのいできましたが、どれも手間がかかり信頼性に欠けるものでした。そこで登場したのがWeb Locks APIです。このAPIの目的は、クライアントサイドで簡易に排他制御を行う仕組みを提供し、複数タブやワーカー間で安全にリソース共有できるようにすることでした。Web Locks APIはW3Cで標準化が進められ、Chromeをはじめとする主要ブラウザで実装されるに至りました。
Webアプリにおけるリソース競合問題:従来技術で直面していた課題とその限界を具体例と共に解説
マルチタブ・マルチスレッド環境では、同じユーザー操作や処理が複数走行することでリソース競合が起こり得ます。例えば、あるウェブアプリでユーザーデータをローカルのIndexedDBに保存し、それをサーバーと同期するケースを考えてみましょう。ユーザーがそのアプリを2つのタブで開いた場合、同期処理が両方のタブから同時に走ると、データの重複送信や書き込み順序の乱れによる不整合が生じる可能性があります。このようなレースコンディションを避けるには、どちらか一方のタブが処理を終えるまで他方が待つ必要があります。
しかし、Web Locks APIが登場する以前は、ブラウザ上でこうした待ち合わせを実現する標準手段がなく、開発者は苦労していました。一つの方法として、localStorageに「ロック中」を示す値を書き込み、他のタブはそれを監視して待機する、といった自前の実装がありました。しかしlocalStorageは厳密な排他を保証するものではなく、タイミングによっては競合を完全には防げません。また、あるタブがクラッシュした場合にロック状態が残ってしまうなど、課題も多く存在しました。このように従来技術では根本的な解決が難しかったリソース競合問題に対し、ブラウザ組み込みで対応すべく作られたのがWeb Locks APIなのです。
ブラウザ内で排他制御を可能にする仕組み:Web Locks APIの基本概念とその動作原理を解説
Web Locks APIでは、「ロック」という抽象的な概念を用いて排他制御を実現します。ロックは文字列で識別され、アプリケーション側で任意の名前を付けることができます。例えば「"my_resource"」という名前のロックを取得すれば、同じオリジン内でその名前のロックを他に取得している処理がない限り、自分の処理が独占的に実行できます。逆に言えば、同一オリジン上で別のタブやワーカーが同じ名前のロックを要求した場合、先にロックを取得している処理が完了するまで、新たなリクエストは待たされることになります。
具体的な仕組みとしては、ブラウザが内部にロックマネージャ(LockManager)を持ち、各オリジンごとにロックの状態を管理しています。スクリプトはnavigator.locks.request()メソッドを通じてロック要求を行い、ロックマネージャはその時点で同じ名前のロックが使用中かどうかを判断します。未使用であれば即座にロックを取得してコールバック処理を実行し、既に使用中であれば要求を待ち行列に入れて待機させます。このようにして、一つのリソース名につき同時に一つの処理(または必要に応じて後述する共有ロックの複数処理)しか実行されないことが保証されます。
ロックは同一オリジン内でのみ有効であり、異なるドメイン間では影響し合いません。また、セキュリティ上の理由からHTTPSなどのセキュアコンテキストでのみ動作する仕様になっています。これらの前提のもと、Web Locks APIはブラウザ内での簡易な排他制御を可能にし、従来難しかった複数環境間の調停処理を実現しているのです。
シンプルなロック機構によって実現できること:他のWeb APIやアプリ機能との関係性を探る
Web Locks APIはブラウザ上における排他制御専用の仕組みですが、他のWeb APIやアプリケーション機能とも補完的に利用されます。例えば、データベースのトランザクション(IndexedDBのトランザクション等)やファイルシステムAPIがそれぞれ独自のロック機構(同時書き込みの防止など)を持っていますが、Web Locks APIはそれらをまたぐような高レベルのシナリオで役立ちます。すなわち、「あるリソースに関わる一連の処理全体」をまとめて単位化し、他の処理と競合しないよう制御することができます。
他の例として、Wake Lock API(画面消灯を防止するAPI)とは全く別物であることに注意が必要です。名前に「Lock」が付くAPIはいくつか存在しますが、Web Locks APIはあくまでアプリ内部の処理調整を目的としたものです。また、Web Locks API自体はUIに直接影響する機能ではありませんが、これを使って裏側のデータ処理を安全に行うことで、結果的にユーザー体験の安定化につながります。
総じて、Web Locks APIは他のWeb APIと競合するものではなく、むしろ不足していた並行処理制御のピースを埋める存在です。従来からあるPromise/asyncによる非同期処理や、Web Workerによるマルチスレッド処理と組み合わせることで、より堅牢でミスの少ないクライアントサイドアプリケーションを構築できるでしょう。
Web Locks APIの具体的な利用イメージ:このAPIが活きるシナリオとはどんな場面か
実際にWeb Locks APIが活躍する場面を一つ挙げてみましょう。例えば、あるウェブアプリでオフライン編集機能があり、編集内容をIndexedDBに保存しつつ一定間隔でサーバーに同期する仕組みがあるとします。ユーザーが同じアプリを複数タブで開いた場合、各タブが独立に同期処理を走らせると、サーバーには重複したリクエストが飛び、無駄な負荷やデータ不整合が生じかねません。そこで、Web Locks APIを使い「sync_lock」というロックを導入します。各タブは同期処理を行う前にnavigator.locks.request("sync_lock", ...)でロックを取得しようとし、取得できたタブだけが同期を実行します。他のタブはロックが解放されるまで待機し、その後順番に同期を行います。このようにして常に1つのタブのみが同期処理を行うよう制御でき、重複送信を防げます。
別のシナリオでは、チャットアプリの「既読」ステータス更新のように、複数タブ間で一度だけ行えばよい処理にも使えます。あるタブがロックを取得して既読をサーバーに送信し、他のタブはその間待つ、といった制御です。こうすることでどのタブからも二重に既読通知を送らない保証ができます。以上のように、Web Locks APIは「ひとつのことを一度だけ行いたい」「同時に走ると不都合な処理を順番に実行したい」という場面で、その真価を発揮します。
Web Locks APIの特徴:排他ロックと共有ロックなどユニークな機能と動作特性について詳しく解説
ここでは、Web Locks APIが提供する具体的な機能や動作上の特徴について掘り下げます。単にロックを取得して解放するだけでなく、Web Locks APIには様々なオプションやモードが存在し、柔軟な制御が可能です。また、デッドロックを防ぐための仕組みも備わっています。それぞれの特徴を理解することで、より適切にこのAPIを活用できるでしょう。
排他ロックと共有ロックの違い:2種類のロックモードの特徴を徹底解説
Web Locks APIには排他ロック(exclusive lock)と共有ロック(shared lock)の2種類のロックモードがあります。排他ロックはその名の通り、ある名前のロックに対して一度に1つの実行コンテキスト(タブやワーカー)しか取得できないロックです。デフォルトではこの排他モードで動作し、典型的な排他制御を実現します。
一方、共有ロックは同じ名前のロックを複数の実行コンテキストで同時に取得可能なモードです。ただし共有ロック同士は共存できますが、共有ロックと排他ロックは同名では同時に取得できません。つまり、あるリソースに対して「読み取り専用」的な操作を複数並行して行いたい場合に共有ロックを使い、書き込みなど排他的に行いたい操作は排他ロックで行う、といったパターンが可能です(これは有名なリーダー・ライターパターンの実装に役立ちます)。
デフォルトではnavigator.locks.request()を呼び出すと排他ロックを要求しますが、オプションで{ mode: "shared" }を指定することで共有ロックをリクエストできます。例えば、
navigator.locks.request("resource", { mode: "shared" }, async () => { // この中の処理は他の共有ロック取得者と並行して実行される });
このように書くと"resource"に対する共有ロックを取得します。複数の箇所で共有ロックを取得すれば、同時に並行実行が可能です。ただし、どこか一箇所でも排他ロックを取得しようとすると、共有ロック取得中の処理は完了するまで待たされる点に注意してください。適切に排他モードと共有モードを使い分けることで、必要な並行性を保ちつつ、データ競合を防ぐことができます。
Lock取得要求のキュー制御:順序保証と公平性を担保する仕組み
Web Locks APIでは、ロックの取得要求が即座に叶わない場合、自動的に待ち行列(キュー)に入ります。例えばあるタブAが"resource"のロックを保持している間に、別のタブBが同じ"resource"のロックを要求したとします。この場合タブBの要求は待機状態となり、タブAがロックを解放したタイミングで初めてタブBにロックが付与されます。さらにその間にタブCが要求してきた場合、BとCの順序はロックマネージャ内部のキューに追加された順番に従って管理されます。つまり、基本的には先にリクエストした順にロック取得の権利が与えられるため、公平性が保たれます。
このキュー制御のおかげで、開発者が自前で待ち処理の実装を行う必要はありません。LockManagerが各ロック名ごとにきちんと順序を制御してくれるため、早い者勝ちで処理が独占されっぱなしになる、といった心配は少ないです。ただし、一部詳細な仕様として、キューの順序や優先度はブラウザ実装に依存する場合があります。一般的な利用においては先入れ先出し(FIFO)の順序で問題ないですが、極端に長時間ロックを保持するタスクがあると、後から来た重要なタスクが待たされるという状況も起こりえます。その場合には、後述するstealオプションやタイムアウトを活用することで対応可能です。通常はロック取得要求は順番待ちとなり、最初のリクエストが解放されたら次が通る、というシンプルなモデルを意識しておけば十分でしょう。
Lockの自動解放とコールバック:Promiseを用いた非同期制御の流れを解説
Web Locks APIの大きな特徴の一つに、ロックの自動解放があります。従来のマルチスレッド環境でのロック(ミューテックスなど)は、明示的に「ロックを解除する」操作が必要でした。しかしWeb Locks APIでは、navigator.locks.request()に渡したコールバック関数の実行が終わった時点で、ブラウザが自動的にロックを解放してくれます。開発者はロックの解放を忘れてしまう心配がないため、デッドロック防止にも一役買っています。
具体的には、navigator.locks.request("名前", (lock) => { ... })の形でコールバックを渡すと、その関数が返り終えた(もしくはasync関数の場合はPromiseがresolveされた)タイミングでロックが解放されます。関数内部で例外が投げられた場合も、最終的には関数が終了したとみなされロックは解放されます。したがって、コールバック内では安心してリソースを独占した処理を書き、処理の最後で自然に関数を抜ければOKです。
なお、navigator.locks.request()自体はPromiseを返すため、呼び出し側でawaitすることで「ロック処理が完了するのを待つ」ことができます。このPromiseは、コールバック関数が終了(ロック解放)した後にresolveされるようになっています。この仕組みによって、例えば:
console.log("A"); await navigator.locks.request("x", async () => { console.log("B"); await doSomething(); console.log("C"); }); console.log("D");
上記のコードでは、常に A → B → C → D の順にログが出力されます。Dが出るのはロックが解放された後であり、BとCの間の非同期処理doSomething()の完了を待ってから次に進むことになります。つまり、Promise/async機構と組み合わせることで、ロックの獲得・解放を含む一連の処理を直列的なフローで記述できるのです。
ifAvailableやstealオプションの活用:条件付き取得や強制取得の機能と活用法を紹介
Web Locks APIのロック要求には、いくつかオプションを指定することができます。代表的なものとしてifAvailableとstealがあります。これらを活用することで、デフォルトの待ち行列に入る挙動を変えることが可能です。
ifAvailable: trueを指定すると、「その時にすぐロックを取得できる場合にのみ取得し、もし既に他がロック中なら待たずに失敗する」動作になります。通常、ロックが取れないと待ちますが、ifAvailable付きの場合は待機せずに呼び出しが終了します。navigator.locks.request(name, {ifAvailable: true}, (lock) => {...})のように使い、ロックが取れた場合はlockオブジェクトが渡され、取れなかった場合はlockにnullが入りコールバックが実行されます。これを利用して、「ロックがすぐ取れなければ今回は処理をスキップする」「別の方法を取る」といった柔軟な対応が可能です。
steal: trueを指定すると、そのロック名で現在保持されているロック(もしあれば)を強制的に解放させて奪い取ることができます。通常は非常に慎重に使うべきオプションですが、どうしても直ちに実行しなければならない重要な処理で、既存のロックを待てない場合に検討されます。stealを使うと、例えば他のタブが長時間ロックを保持している状況でも、自分の処理が割り込んでロックを取得できます。ただし、その場合先行していた処理は途中でもロックを失う(厳密には、以降は排他性が保証されなくなる)ため、データの一貫性が損なわれる恐れがあります。従ってstealは本当に緊急の場合か、あるいはデバッグ用の手段として用い、通常は使わなくても済む設計が望ましいでしょう。
他にもsignalオプションでAbortSignalを渡せば、一定時間でロック要求を打ち切るタイムアウト実装が可能です。こうしたオプション群を活用することで、Web Locks APIの挙動を細かく調整し、アプリケーションに適した動きを実現できます。
デッドロック防止の仕組み:ネスト回避やタイムアウト処理など対策を解説
複数のロックを扱うケースではデッドロック(お互いに相手のリソース解放を待って停滞してしまう状態)に注意が必要です。Web Locks API自体はデッドロックを完全に防止する仕組みを持っているわけではありませんが、開発者が適切に設計することで回避できます。典型的には「ロックのネスト(入れ子)取得」を避けたり、取得順序を工夫することが重要です。例えばタブAでロック"A"を取得中にロック"B"を要求し、タブBで"B"取得中に"A"を要求する、という状況になると両者が互いに待ち続けます。これを避けるには、常に同じ順序でロックを取得する(例えば必ず"A"→"B"の順に取得し、逆順を許さない)などのルールを決めれば良いでしょう。
また、前述のifAvailableやAbortSignalを用いたタイムアウト設定もデッドロック予防に有効です。一定時間ロックが取れなければ諦めて処理をキャンセルすることで、システム全体が停止したままになる事態を避けられます。さらにstealオプションも最終手段としてはデッドロック解消に役立つ可能性があります。もっとも、stealで強制取得するということは、他方の処理を強制的に中断させるようなものなので、後始末が難しく推奨はされません。
幸い、Web Locks APIで単一のロック名を使う限りはデッドロックの心配はありません。問題は複数の異なるロック名を同時に扱う場合だけです。複雑なケースではシステム設計段階で「ロックの競合グラフ」を考え、デッドロックの可能性を排除することが求められます。Web Locks API自体は軽量なツールなので、最終的な安全性は私たち開発者の設計に委ねられている点を覚えておきましょう。
Web Locks APIの使い方:LockManagerを用いた基本的な非同期ロック取得の手順を解説
ここからは、実際にWeb Locks APIを使ってロックを取得・解放する基本的な手順を説明します。APIの構造やメソッドの使い方、コールバックの書き方など、実装に必要な基礎知識を順を追って見ていきましょう。
Web Locks APIを利用する際のエントリポイントとなるのが、ブラウザが提供するLockManagerオブジェクトです。各ページ(もしくはワーカー)はnavigator.locksというプロパティを通じて、このLockManagerにアクセスできます。navigator.locksはNavigatorインターフェイスに追加されたプロパティで、読み取り専用でLockManagerのインスタンスを返します。開発者はこれ以上特別な初期化等は必要なく、単純にnavigator.locksを使い始めればOKです。
LockManagerには、主にロックをリクエストするためのrequest()メソッドや、現在のロック状態を調べるquery()メソッドなどが用意されています。request()がWeb Locks APIの中核となるメソッドであり、後述するようにこれを呼ぶことで実際のロック取得とコールバック処理が行われます。LockManagerはNavigator以外にもWeb Worker内ではworkerNavigator.locksとして同様に利用可能で、メインスクリプトでもワーカー内でも同じようにロックを扱えます。
まとめると、Web Locks APIを使う際にはまずnavigator.locksにアクセスし、そこから提供されるメソッドを呼び出すことになります。LockManager自体をコンストラクタで生成したりする必要はなく、ブラウザが提供するものをそのまま使うというシンプルな構造です。
ロックを取得するにはnavigator.locks.request()メソッドを使用します。その基本的な呼び出し構文は次の通りです:
navigator.locks.request(名前, オプション, コールバック関数);
3つの引数を取りますが、このうちオプションは省略可能です。第一引数の名前にはロックの識別名を表す文字列を指定します。任意の文字列で構いませんが、アプリ内で衝突しないようなわかりやすい名前を付けることが大切です。第二引数のオプションには、前述したmodeやifAvailable等を含むオブジェクトを指定できます。特に指定しなければデフォルト動作(排他ロック・待ちあり)となります。第三引数のコールバック関数は、ロックを取得した際に実行される関数です。この関数には引数としてLockオブジェクトが渡されますが、通常あまり使わず、もっぱらロック中に行いたい処理を関数本体に記述します。
例えば基本的な使い方として:
navigator.locks.request("update", async (lock) => { // ロック「update」を取得している間だけ実行される処理 await performUpdate(); });
このように書くことで、名前"update"のロックを取得してperformUpdate()という非同期処理を実行します。他に同じ"update"ロックを使おうとする処理がなければすぐにコールバック内が実行されますが、もし別タブなどで既に"update"ロックを保持中であれば、そのロックが解放されるまでこのrequest呼び出しは待機します。ロック取得に成功すると、コールバックの先頭でロックオブジェクトが渡ってきます。必要ならlock.nameやlock.modeといったプロパティで情報を参照できますが、単一のロック名をハードコーディングしている場合はあまり使わないでしょう。
なお、request()はPromiseを返すため、上記のようにawaitを付けています。Promiseが解決するタイミングは、先述の通りコールバック処理が完了してロックが解放された時点です。従ってrequest()以降のコードは、排他処理が終わった後に実行されます。このPromiseの性質を活かすと、ロック処理を含めた一連の流れを順序立てて書けるため、複雑な同期処理も比較的読みやすいコードにできます。
コールバック関数内での処理:ロック獲得後に行うべき作業の例と注意点
コールバック関数内では、実際に他と競合させずに行いたい作業を記述します。例えばデータの変更や、ネットワークとの同期、ファイル操作などが典型です。コールバックはロックを獲得してから呼ばれるため、この中の処理は同じロック名に関しては排他が保証されています。他のコンテキストが割り込んでこないことを前提に、安全にリソースを操作できます。
コールバック関数自体は同期関数でも非同期関数(async)でも構いません。非同期関数にすれば、中でawaitを使ってさらに別の非同期処理(例えばfetchでのネットワーク呼び出しや、IndexedDBへの書き込みなど)を行うことができます。ロックはコールバックの終了まで保持されるため、awaitで待っている間も他からロックは奪われません。ですから、安心して一連の処理を順番に書いていけば良いわけです。
コールバック内の処理で注意すべき点は、できるだけロックを保持する時間を短くすることです。長時間に渡ってロックを保持したままだと、待っている他の処理が遅延してしまいます。例えば大量の計算を伴う処理をロック内で行うと、その間他タブの操作がブロックされ、ユーザー体験を損ねる恐れがあります。このような場合は、重い計算部分はWeb Workerに任せ、ロック内では結果の反映だけを行うなど、クリティカルセクション(排他区間)をなるべく短く保つ工夫が必要です。
また、コールバック内で例外(エラー)が発生しうる場合は、そのハンドリングも重要です。ロック自体はエラーの有無に関係なくコールバック終了時に解放されますが、エラーが起きると後続の処理が行われずに終了してしまいます。他の待機している処理は順番が来れば動きますが、アプリケーションとしてはエラーを検知して適切に対処することが望ましいです。したがって、ロック内で起こりうるエラーはtry...catchで捕捉し、必要に応じてログやリカバリ処理を入れておくと良いでしょう。
Promise/awaitによる直列実行:非同期処理を同期的な流れで書く方法
前述したように、Web Locks APIのコールバックはPromiseを返し、その完了を待って次の処理に進むことができます。この性質を使えば、非同期処理をあたかも同期処理のように順番通りに記述することが可能です。具体的には、ロックを使った処理の前後にawaitを配置することで、プログラム上の実行順序を保証できます。
例えば、「ロックなしでできる処理A → ロックが必要な処理B → 再びロックなしでできる処理C」という3つのステップがあるとします。この場合、
// ステップA(ロック不要部分) doStepA();
// ステップB(ロックが必要な部分) await navigator.locks.request("lockname", async () => { doStepB_part1(); await doStepB_part2(); });
// ステップC(ロック不要部分) doStepC();
と書くことで、必ずA→B→Cの順に実行されます。特に、B部分はロックを取っているため並行して他の同種処理は走りませんし、awaitによりCはBが完全に終了するまで開始されません。これにより、時間的な順序や依存関係が複雑な処理であっても、コード上は上から下へと順番に書き下すことができます。複数のPromiseやコールバックが入り組むケースでも、Web Locks APIとasync/await構文を組み合わせれば、直観的な直列フローで表現可能です。
このようなコード構造は、読み手にとっても分かりやすくデバッグしやすい利点があります。ただし、あまりに長い直列の流れを1つの関数に詰め込むとかえって把握しづらくなるため、適宜関数を分けるなど工夫は必要です。Web Locks APIはその一部として組み込むことで、全体の流れの中でクリティカルな部分だけを順序制御する役割を果たすと考えると良いでしょう。
ロックの自動解放タイミング:コールバック完了時に何が起こるか
ロックの自動解放については既に触れましたが、そのタイミングと挙動を改めて整理します。コールバック関数(navigator.locks.requestに渡した関数)が最後まで実行され終えるか、もしくはreturn文で明示的に終了した瞬間に、LockManagerは現在のロックを解放します。これは非同期関数の場合も同様で、関数がPromiseを返すならそのPromiseがresolve(またはreject)された時点が解放のタイミングです。
ロック解放が行われると、同じロック名で待機中の次の要求があればただちにそのロックが付与され、対応するコールバックが実行されます。何も待っていなければそのロック名は単に「フリーな状態」に戻ります。なお、一度解放されたロックは明示的に再度解放を呼び出す必要はなく、LockManagerがすべて管理します。
開発者側から見ると、「いつロックが解放されるか」を意識してコードを書く必要は基本的にありません。コールバック内の処理が終われば自動で解放されますし、逆に言えば処理が終わらない限りはずっと保持されます。このため、コールバック内で意図せず無限ループに陥ったり、awaitしているPromiseが永遠に返ってこなかったりすると、ロックが長時間にわたり掴みっぱなしになってしまう点には注意しましょう。そのような事態を避けるために、AbortSignalでタイムアウトを設定したり、コールバック内で異常が起きたとき速やかに抜ける工夫が重要です。
逆に正常にコールバックが終了した場合は、何も気にせずロックが解放されます。その後に別の処理をしたければ、request()の戻り値Promiseに対して.thenを続けたり、前述のようにawaitした後のコードを書いたりすればよいです。このように、ロックの解放タイミングはコールバックと1対1に対応しており、直感的に把握できる仕様になっています。
Web Locks APIの基本的な使い方と実装例:シンプルなコードで見るロック取得と解放の方法を解説
それでは、Web Locks APIを使った簡単な実装例を見てみましょう。ここでは、先ほど説明した概念を踏まえて、実際のコードがどのように書けるのかを具体的に示します。シンプルなユースケースを題材に、ロックの取得から解放までの流れを一通り実装してみます。
シンプルなデータ同期シナリオにおけるWeb Locks APIの利用例を紹介
例として、「複数タブ間でのデータ同期」シナリオを考えます。前述の利用イメージでも触れたように、複数のタブが同じデータベースやサーバーと同期を行う場合、Web Locks APIを使って排他制御することで効率と整合性が向上します。今回の実装例では、擬似的にIndexedDBへのデータ書き込みとサーバーへの送信を同期する処理を想定します。
具体的には、各タブで「同期」ボタンが押されるとデータをIndexedDBに保存し、さらにその内容をサーバーに送るという機能があるとします。このとき、複数タブから同時に送信すると二重送信になりかねないため、Web Locks APIで「sync_lock」というロックを用意し、どれか一つのタブのみが送信処理を行うようにします。他のタブは自分のIndexedDB保存だけ行い、サーバー送信は最初のタブに任せる、という役割分担です。
では、このシナリオをベースにコードを見てみましょう。
Lock取得前の準備:事前条件とチェック処理のポイントを解説
まず、ロックを取得する前にいくつか準備やチェックを行います。一つは、Web Locks APIが利用可能かどうかの検出です。万一ユーザーのブラウザが未対応の場合、navigator.locksは存在しません。そのため、以下のような確認を入れておきます。
if (!('locks' in navigator)) { console.warn("Web Locks API未対応のブラウザです。同期処理をスキップします。"); // フォールバック処理やエラー表示をここで行う } else { // Web Locks APIを使った同期処理へ }
このようにnavigator.locksの有無をチェックし、無ければ代替手段を取るのが望ましいです。代替手段としては、最悪何もしない(機能を提供しない)か、簡易的に全タブで処理するがユーザーに注意を促す、といった対応が考えられます。
もう一点、ロック前に準備しておくこととして、同期対象のデータ取得があります。例えば送信すべきデータがメモリ上にあるならそれを用意し、IndexedDBから読み出す必要があるなら事前に読んでおくと、ロックを持っている時間を短縮できます。準備が整ったら、いよいよロックの要求に移ります。
Lock取得と排他処理の実装:実際のコードで学ぶロック確保の手順
では、ロックを取得して実際に同期処理を行うコードを示します。
async function syncData(data) { // 1. IndexedDBへの保存(ロック不要なので先に実施) await saveToIndexedDB(data); console.log("ローカル保存完了");
// 2. サーバー同期は排他制御 await navigator.locks.request("sync_lock", async () => { console.log("サーバー同期処理開始(ロック取得)"); await sendToServer(data); console.log("サーバー同期処理完了(ロック解放)"); });
console.log("同期処理全体完了"); }
この関数syncDataは、与えられたデータをまずIndexedDBに保存し(ここはロック不要なので即時実行)、その後"sync_lock"というロックを取得してサーバー送信処理sendToServerを実行します。サーバー送信部分はロックで囲まれているため、他のタブで同じsyncDataが呼ばれても、必ず一つずつ順番にsendToServerが実行されることになります。
コード中のログ出力"サーバー同期処理開始(ロック取得)"と"サーバー同期処理完了(ロック解放)"に注目すると、別タブでこの関数を実行した場合、それぞれのタブでログが交互に出力されることがわかります。一つのタブが「開始」して「完了」するまで、他のタブでは「開始」に入れず待機するのです。これがWeb Locks APIによる排他制御の効果です。
このように、実装としてはnavigator.locks.requestで囲むだけで、特に難しいことはしていません。あとはアプリケーションの要件に合わせて、送信以外に排他が必要な処理があれば同様に囲むなど、適用範囲を決めて使っていけば良いでしょう。
Lock解放後の処理:ロック完了後の後続タスクへの受け渡し方法
ロックで保護された処理が終わった後に何か後続のタスクがある場合、その処理をどこで行うか検討が必要です。上記のコードでは、IndexedDB保存→サーバー送信→全体完了 という順序がawaitで保証されており、サーバー送信(ロック部分)が終わったらその次の行console.log("同期処理全体完了")が実行されています。このように、ロック解放後の処理をすぐ後ろに書く方法は簡潔でわかりやすいです。
しかし場合によっては、「ロックを取っている間に他のタブが待っていた」という状況で、待っていた側に何か通知を送りたいこともあるでしょう。例えば、最初にロックを取ったタブがサーバー送信を済ませたら、他のタブに「同期完了」の合図を送るイメージです。その場合は、ロック解放直後に何らかの共有メカニズムで通知を行います。具体的には、localStorageの値を書き換えてstorageイベントを発火させたり、BroadcastChannelでメッセージをブロードキャストするといった方法があります。
このように、ロック処理後に別のコンテキストへ情報を伝達する必要があれば、Web Locks APIと他の通信手段を組み合わせることになります。Web Locks API自体は通知機能を提供しないので、そこは従来通りの方法で補完してください。一方、単に同じ処理を順番に行うだけであれば、何も特別なことはせず順番待ちに任せればOKです。
フォールバックの実装:未対応ブラウザへの対策としての代替処理
最後に、Web Locks APIが使えない環境へのフォールバックについて触れておきます。現時点(2025年)で主要ブラウザはほぼ対応済みですが、古いInternet Explorerなどでは利用できません。そうした場合に備え、コード上で何らかの代替動作を用意するのが理想です。
フォールバックの戦略としては、次のようなものが考えられます。
- 単純に諦める: ロックが使えない場合は競合が起きるリスクを承知で通常処理を行う。ユーザーへの注意喚起メッセージを出す程度。
- 機能を無効化: ロックが使えない環境ではその機能(例えばオフライン同期機能自体)を提供しない。
- 簡易ロック実装:
localStorageやCookieを用いて簡易的なロックまがいの実装を入れる。
どれを選ぶかはアプリの性質によりますが、安全なのは後者2つでしょう。特にエンタープライズ向けでIEサポートが必要な場合などは、Web Locks APIに依存しない設計にすることも検討しなければなりません。いずれにせよ、navigator.locksが存在しない場合にどう動くかを考えておくことで、予期せぬエラーを防ぎ、より堅牢な実装になります。
Web Locks APIの利用シーン・ユースケース:複数タブでのデータ同期から共同編集まで幅広く紹介
Web Locks APIは具体的にどのような場面で役立つのでしょうか。ここでは、このAPIの典型的なユースケースをいくつか紹介します。マルチタブ環境やマルチスレッド環境での問題解決に加え、他にも応用できるシナリオを見てみましょう。
ブラウザ間でのデータ同期:複数タブの操作を調整するケースの解説
最もわかりやすいユースケースが、複数タブ間でのデータ同期です。ユーザーが同一アプリを複数タブで開いている状況では、例えば設定変更やメモの保存などの操作が各タブで競合する可能性があります。Web Locks APIを使えば、あるタブがデータ保存処理を行っている間、他のタブではその処理が完了するまで待つようにできます。先ほど実装例で示した同期処理もこの一種です。
特にオフライン対応のアプリでは、ローカルデータとサーバーデータの整合性を保つために同期処理が欠かせません。複数タブが独立に同期すると重複送信や衝突が起きますが、Web Locks APIで同期処理を直列化すれば「常に最新のデータを一度だけ送る」ことが可能になります。これによりサーバー側でも重複受信の処理を簡略化でき、全体として効率が上がります。
複数タブでのリソース競合防止:排他制御が必要な典型例を紹介
データ同期以外にも、複数タブでの操作競合を防止したい場面は多数あります。例えばユーザーがECサイトの商品ページを複数開き、同じ商品を二重に購入してしまうケースを考えてみます。本来それを防ぎたい場合、購入手続き処理にロックをかけておけば、最初のタブが処理中に二番目のタブは待たされます。結果、二重購入ボタン押下のような状況でも、一件ずつ順番に処理されますから、在庫引当や決済処理の重複を防げます。
また、ゲーム等のリアルタイム性の高いアプリでは、複数ウィンドウで同じアクションを起こされると不具合が出る場合があります。例えばRPGゲームで同じキャラクターを2つのタブから同時に動かすと、サーバー側で状態が混乱するかもしれません。そうした場合にも、クライアント側でロックを用いて「一人一行動ずつ」に制限することができます。このように、複数タブで同じ資源(キャラクターや在庫など)を触る恐れがある場合、ロックによる排他が有効な解決策となります。
Web Workerを含むマルチスレッド処理:バックグラウンドタスクの協調実行例
Web Locks APIは、Web WorkerやService Workerなどバックグラウンドスレッドとの協調にも役立ちます。たとえば、メインスクリプトとワーカーが両方とも同じIndexedDBを更新しうる場合を考えましょう。通常IndexedDB自体はシリアライズされたトランザクションを提供しますが、論理的な処理単位でも排他制御したいケースがあります。そのようなとき、メインとワーカーで同じロック名を使えば、一方がロック中は他方の処理を待たせ、順番に実行させることができます。
実例として、あるPWA(プログレッシブウェブアプリ)がメインスレッドと2つのWeb Workerを持っているとします。このアプリはIndexedDBにデータを保存しつつ、ネットワークとも同期しています。通常IndexedDBは単一のコンテキストからの操作は直列化されますが、3つのコンテキスト(メイン+2ワーカー)から同時操作があると競合が懸念されます。そこでWeb Locks APIを使い、すべてのデータ同期処理で同じロックを取るようにします。こうすることで、3つのスレッドがあってもデータの同期処理自体は一度に一つだけ走るようになり、競合と不整合を避けられます。
このように、Web Worker等バックグラウンドで動く処理ともWeb Locks APIは連携可能です。メイン・ワーカー間で簡易な排他プロトコルを築ける点は、複雑な並行処理を扱う開発者にとって非常に有用です。
共同編集アプリへの応用:リアルタイムコラボレーションでの利用シーンを解説
Googleドキュメントのようなリアルタイム共同編集アプリでは、複数ユーザーが同時にドキュメントを編集します。一見関係なさそうですが、例えば一人のユーザーが同じドキュメントを複数タブで開いて編集するケースがあります。通常、コラボレーションはサーバーが調停しますが、クライアント側でも不要な競合を避けたいものです。Web Locks APIはこうしたケースにも応用可能です。
例えば、同じドキュメントIDに対して「編集ロック」を導入し、ユーザーのいずれか一つのタブだけが積極的にサーバーと同期するようにします。リードオンリーな更新受信は全タブで行っても問題ありませんが、ユーザーからの編集送信は一つに絞った方が効率的です。そこで編集開始時にnavigator.locks.request("edit_doc_123", {mode: "exclusive"}, ...)のようにして、あるタブがリーダー役を担うイメージです。他のタブはそのロックが取れない場合は、自身では送信せず待機するか、リーダータブに編集内容を委任するといった動きになります。
実際の実装ではWebSocket等との兼ね合いもあり単純ではありませんが、クライアント内でリーダー選出(リーダーエレクション)をする一つの手段としてWeb Locks APIを用いることができます。この点については、Web Locks APIを用いたリーダー選出パターンとして文献でも紹介されています。
その他の実用例:タスクスケジューリングや一時的な排他処理への応用
上記以外にも、Web Locks APIはアイデア次第で様々な用途に使えます。例えば、Periodic Background Sync(定期的なバックグラウンド同期)やPush通知のハンドリングなど、Service Workerが複数イベントを同時処理し得る場合に、Web Locks APIでシリアライズするという手があります。Periodic Syncでは、ネットワーク接続状況によって同じ同期が重複起動しないようにロックで制御するといったことが考えられます。
また、リソース消費の大きな処理を間欠的に実行する際にも役立ちます。例えば、大きなファイルのエンコード処理をユーザーが何度もキューに入れた場合、Web Locks APIで一度に一つずつ実行するようにすることで、CPU使用率の高騰やメモリ不足を防げます。このようなケースでは、単に排他というよりは「同時に走らせない」というスケジューリングの目的でロックを使っています。
さらに、アプリケーション内の一時的なモード切替にロック概念を流用することも可能です。例えば音声アプリで「録音モード」は排他だと考え、複数の録音操作が同時に走らないようロックを取得・解放で管理する、といった応用です。UI上で録音ボタンを押すとロック取得、録音終了でロック解放し、別の録音開始ボタンはロックが取れない間グレーアウトする、といった制御ができます。
このように、Web Locks APIは「同時に一つ」という制約をかけたいあらゆる場面で利用可能です。発想次第で便利なユースケースがまだまだ広がるでしょう。
Web Locks APIのメリットとデメリット:活用する利点と注意すべき欠点をそれぞれ詳しく解説する
Web Locks APIを導入することで得られるメリットと、逆に注意すべきデメリットについて整理します。新しい技術には利点だけでなく欠点も存在します。ここではそれらを比較し、どのような場合にこのAPIを採用すべきか判断する助けとします。
Web Locks APIを利用する主なメリット:実装簡易化やデータ整合性向上など利点のまとめ
最大のメリットは、ブラウザ内での排他制御を簡潔に実装できる点です。従来なら開発者自身が実装していた複雑な競合回避ロジックを、Web Locks APIが肩代わりしてくれます。単に数行のコードを書くだけで、複数タブ・複数スレッド間の同期が取れるのは非常に強力です。特に複雑なタイミング調整やフラグ管理が不要になり、コードの見通しが良くなる実装の簡易化は見逃せない利点でしょう。
次に、データの一貫性や整合性が保ちやすくなるというメリットがあります。同時実行を許すと起こりがちなレースコンディションを排除できるため、予期せぬバグやデータ破損の可能性を減らせます。これはアプリの信頼性向上につながります。たとえば先の例で二重送信が防げることで、サーバー側の処理も正しく一回だけ実行され、データがダブらないという恩恵があります。
さらに、Web Locks APIはブラウザ標準の機能であるため、ポリフィルやサードパーティライブラリに頼らない純粋な実装が可能になります。追加の依存関係がないのはメンテナンス性・パフォーマンスの面でもプラスです。仕様が標準化されている安心感もあり、ブラウザのアップデートによって大きく挙動が変わる心配も少ないでしょう。
Web Locks APIがもたらす利点:レースコンディション防止やパフォーマンス最適化への寄与
Web Locks APIを使うことで得られる具体的な利点として、まずレースコンディションの防止が挙げられます。複数の処理が競合する状況を強制的に直列化できるため、「たまたま早かったほうが先に実行されて不整合が…」という問題を解消できます。レースコンディション由来のバグは検出が難しく厄介ですが、そもそも並行させないことで根本から絶つというアプローチです。
また、一見矛盾するようですがパフォーマンスの最適化にも寄与する場合があります。無秩序に並行処理を実行すると、CPUやネットワークを奪い合って却って効率が下がるケースがあります。Web Locks APIで適度に順序制御することで、システム資源を有効活用できる場合があるのです。例えば、大量の書き込み要求が同時に来たとき、一つずつ処理したほうがディスクへの負荷が平準化されて速く終わる、といったことは現実に起こります。
さらに、Web Locks APIはエラーハンドリングやタイムアウト戦略と組み合わせることで、堅牢な制御が可能です。Promiseベースで書けるので、.catchでエラー処理を入れたり、AbortSignalで時間制限をつけたりといった改善も容易です。これにより、単に動くだけでなくエッジケースにも強い実装を比較的シンプルなコードで書けます。これらは開発者・ユーザー双方にメリットとなるでしょう。
Web Locks API導入のデメリット:ブラウザ互換性や実装コストなど考慮すべき課題
一方、Web Locks APIの導入にはいくつかのデメリットや課題も存在します。まず挙げられるのがブラウザ互換性の問題です。現在では主要なモダンブラウザは対応していますが、古いブラウザ(特にInternet Explorerなど)はサポートしていません。そのため、ユーザー層によっては一部で機能しない可能性があります。これを無視する場合、未対応ブラウザでは機能が動作せず不具合となるため、フォールバック処理など追加の実装コストが発生します。
また、API自体が比較的新しく開発者への認知度がまだ高くないため、チーム内で導入を検討する際に学習コストがかかる可能性があります。ライブラリではなくブラウザ標準とはいえ、新しい概念をプロジェクトに持ち込むわけですから、理解不足による使い方の誤りなども起こりえます。特にデッドロックやロック取得忘れ(実際には自動解放なので起こりませんが、構造的に抜け漏れがある場合)などに気を配る必要があり、完全に放置して良いものでもありません。
さらに、Web Locks APIによる制御そのものが抱えるデメリットもあります。ロックで待ちが発生するということは、裏を返せば並列処理の機会損失になります。適切でない箇所まで何でもロックしてしまうと、本来同時に実行できた処理まで順番待ちになり、全体のスループットが低下する恐れがあります。利便性と引き換えに、そこはトレードオフとして考慮しなければなりません。
Web Locks API使用上の懸念点:ロック競合時の待ち時間増大やデッドロック発生の可能性に注意
Web Locks APIを使うことでかえって生じる懸念点も押さえておく必要があります。まず、複数の処理が頻繁に同じロックを取り合う状況では、待ち時間が増大する可能性があります。例えば、高頻度で発生するイベントに対して常にロック制御をしていると、実行待ちのキューが長くなり、ユーザー体験としてタイムラグが大きくなってしまうかもしれません。従って、本当に排他が必要な部分だけに絞ってロックを使うなど、過度な利用は避けることが大切です。
また、設計・実装次第では前述したデッドロックの可能性がゼロではありません。単純なケースでは起こりませんが、複数種類のロックをネストしたり、他システムとの相互作用がある場合など、想定外の相互待ち状態が発生しないよう十分注意する必要があります。Web Locks API自体にはデッドロック検出機能は無いため、発生しても黙ったまま処理が止まってしまいます。これを防ぐには、開発者がロック設計を慎重に行うか、タイムアウトを設定して強制脱出する仕組みを組み合わせるしかありません。
さらに、stealオプションの濫用も懸念されます。stealは強力ですが、下手に使うと排他制御の意味を損ないかねません。例えばある処理Aがロック中に処理Bがstealで奪い取ると、AとBが同時並行で動く状況になってしまい、本末転倒です。stealは本当に必要な場合に限定し、通常は使わない方針で運用すべきでしょう。
以上のように、Web Locks APIは便利な反面、その性質上「待ち」や「停止」を伴うため、使いどころを誤るとパフォーマンス悪化やデッドロックといった弊害を招きます。導入時にはこれら懸念点を念頭に置き、適切な粒度でロックを設計することが求められます。
適用判断のポイント:メリットとデメリットのバランスをどう評価するか
以上のメリット・デメリットを踏まえて、実際にWeb Locks APIを採用すべきかどうか判断するポイントをまとめます。基本的には、「想定される並行実行の問題を、導入コストを上回る効果で防げるか」が鍵となります。
具体的には、複数タブや複数スレッドでの競合が実際に起こりうるアプリケーションであり、その影響が大きい場合は導入を検討すべきでしょう。逆に、常に単一タブでしか使われないシンプルなWebサイトであれば、Web Locks APIを使う必要はありません。また、ユーザー利用環境に古いブラウザが多く含まれる場合も、フォールバック実装の手間を考慮する必要があります。
プロジェクトチーム内で技術習熟度も判断材料です。新しいAPIに対する学習コストやレビュー体制が整っているかも考慮します。Web Locks APIは比較的直感的で簡単とはいえ、プロジェクトの他のメンバーが知らなければ、ドキュメント整備や周知が必要です。
最終的には、Web Locks API導入によって得られるデータ整合性や実装容易性のメリットが、潜在的なデメリット(パフォーマンスや対応範囲)を上回るかを評価します。もし利点が大きければ採用し、そうでなければ従来通りの手段で済ませるのも一策です。その判断のために、本節で挙げたポイントを是非役立ててください。
Web Locks APIの対応ブラウザと利用上の注意点:サポート状況と導入前に知っておきたい事項を解説
新しいWeb APIを採用する際には、対応ブラウザの状況や仕様上の制約を把握しておくことが重要です。ここではWeb Locks APIがどのブラウザで利用可能か、また使う上で知っておくべき注意点について説明します。
主要ブラウザでのサポート状況:Chrome・Firefox・Safari等の対応バージョンを確認
Web Locks APIは、2025年現在ほとんどの主要ブラウザでサポートされています。具体的には、Chrome 69以降、Firefox 96以降、Safari 15.4以降、およびそれらのエンジンを搭載したEdgeやOpera、モバイルブラウザ(Android Chromeなど)でも利用可能です。現行バージョンのブラウザであればまず問題なく動作すると考えてよいでしょう。
サポート状況を数字で見れば、世界全体のユーザーの約94%前後がWeb Locks APIを利用できる環境にあります。これはかなり高い普及率であり、新規のWebアプリで採用する分には大きな支障はないレベルです。ただし、残り数%の環境では使えないことも意味しますので、次節で述べるような例外には注意が必要です。
Internet Explorerでは未サポート:旧環境での挙動と推奨される対応策
Internet Explorer (IE)は、残念ながらWeb Locks APIに対応していません。また、旧Edge(EdgeHTML版)や古いAndroidブラウザなどもサポート外です。これらのブラウザでnavigator.locksにアクセスしようとするとundefinedであり、何もできません。前述のフォールバック処理の節で触れたように、IEをサポート対象に含めるなら代替処理を考える必要があります。
実際には、IE自体が公式サポート切れとなり、ほとんどのサイトで推奨ブラウザから外れてきています。そのため、「IEでは本サービスをご利用いただけません」と案内する選択肢も十分考えられます。どうしてもIEで類似の機能を実現したい場合は、サーバーサイドでロックを行うか、localStorageを使ったごく簡易的な排他制御をスクリプトで組むなどの苦肉の策が必要でしょう。しかしそれも完全な代替にはなりません。
したがって、現代のWebアプリ開発においては、IEなどを切り捨ててWeb Locks API等の最新機能を活用するか、あるいはIEも含めて動作するより古典的な設計にするかの判断が迫られます。多くの場合、ターゲットユーザーが一般向けであればIE無視で問題ないですが、社内システムなどでIEが根強く使われている環境では注意が必要です。
HTTPS必須など利用前提条件:セキュアコンテキストでのみ動作する仕様を理解
Web Locks APIを使用するには、セキュアコンテキストである必要があります。要するに、サイトがHTTPSで提供されていることが前提です(ローカルホストは例外的に許可されます)。HTTPのままのサイトではnavigator.locksは存在しない扱いになるため注意してください。セキュアコンテキスト制限は近年の新しいWeb APIに共通の制限で、ユーザーの安全を守る観点から、潜在的に影響の大きい機能はHTTPSでのみ有効になるよう設計されています。
また、同じく前提条件として同一オリジン内でのみロックが有効という点も改めて注意しましょう。サブドメインが異なると別オリジンと見なされるため、app.example.comとapi.example.comではロックを共有できません。これはCookieのスコープとは異なる点なので混同しないようにしてください。例えば、あるWebアプリが別ドメイン上のiframe内で動作している場合、親ページとiframeでロックを共有することはできません。
要件としては、この他に特別な権限やフラグ設定等は不要です。以前は実験的機能だったためChromeでオリジントライアル(試用登録)やフラグが必要でしたが、現在は標準機能となっています。ただ、古い情報を参照して誤解しないように注意してください。
ロックスコープの範囲:オリジン単位で影響が限定される仕様とその意味
Web Locks APIのロックはオリジン単位で管理されます。同じオリジン(スキーム・ホスト・ポートが全て同じ)のページやワーカー間ではロックを共有しますが、オリジンが異なれば完全に別物として扱われます。例えば、https://example.com上のロックはhttps://sub.example.comには何の影響も与えません。また、https://example.com:8080(ポート違い)も別オリジンなので互いに干渉しません。
この仕様の意味するところは、Web Locks APIはあくまで単一オリジン内の協調にフォーカスしているということです。複数サイト間の排他制御まではカバーしません。セキュリティ面を考えると当然ではありますが、もし複数サイトにまたがる制御が必要な場合は、サーバーサイドでの調整や、ブラウザ以外のシステムでのロックが必要になるでしょう。
一方で、同じオリジン内であれば、タブ・iframe・Service Worker・Web Workerといった区別なく全てのコンテキストでロックが共有されます。これにより、例えばService Worker内のバックグラウンド同期と、ユーザーが操作するフォアグラウンドの処理との間でもロックを使って協調可能です。このスコープの広さはWeb Locks APIの強みと言えます。スクリプト実行環境が違っても、同じWebアプリなら足並みを揃えられるのです。
性能と設計上の注意事項:長時間ロックの回避やロック名設計のベストプラクティス
Web Locks APIを使う際のパフォーマンス面・設計面での注意事項をいくつか挙げます。
- 長時間ロックの回避: 一度ロックを取得したら、できるだけ短時間で解放するのが理想です。長時間ロックを保持すると他の処理が待たされ、全体のスループットが下がります。重い処理は分割したり、必要なら途中でロックを解放して他に譲ることも検討しましょう。
- ネストしたロック取得に注意: 複数のロックを連続で取得する場合、順序を統一するなどデッドロックを避ける配慮が必要です。可能な限り単一のロックで用が足りるよう設計し、無闇に複数ロックをネストしないのがベストプラクティスです。
- ロック名の設計: ロック名は同じオリジン内でユニークかつ意味のあるものにします。他の機能と偶然名前がぶつからないよう命名規則を決めると良いでしょう。例えば機能ごとにプレフィックスを付ける(
"featureA_task1"のように)ことで衝突を防ぎます。 - フォールバックの考慮: 再度になりますが、API未対応環境でどうするかを考えて実装することも大事です。限定的なサポートでも大きな問題にならない場合は割り切ってしまえますが、重要な機能であれば代替処理の実装コストも織り込んでおきます。
- デバッグと可視化: 複数コンテキストでのロックは挙動が見えにくいため、
navigator.locks.query()を活用して状態をロギングするなど、開発時に可視化するとバグを発見しやすくなります。
以上のポイントを踏まえて設計・実装すれば、Web Locks APIをより効果的かつ安全に活用できるでしょう。
Web Locks APIの具体的なサンプルコード集:高度な機能を使った実践例コードを紹介して解説する
最後に、Web Locks APIの高度な使い方を示すいくつかのサンプルコードを紹介します。排他制御の応用編として、共有ロックの利用やifAvailable・stealオプションの使いどころ、LockManager.query()による状態確認、さらにはデッドロック対策のコーディングパターンなどを見てみましょう。
共有ロックの利用例:複数の読み取りタスクを同時実行するサンプルコード
まず、共有ロック(mode: "shared")の利用例です。共有ロックは一つのリソースに対して複数の処理が同時にロックを保持できるため、主に「読み取り」のような排他が不要な場面で役立ちます。
例として、あるデータに対する読み込み処理AとBを並行して実行しつつ、その間に書き込み処理Cは待機させる、というシナリオを考えます。
// 読み取り処理A navigator.locks.request("data", { mode: "shared" }, async () => { console.log("A: データ読み取り開始"); const resultA = await readData(); console.log("A: データ読み取り完了", resultA); });
// 読み取り処理B(並行して実行) navigator.locks.request("data", { mode: "shared" }, async () => { console.log("B: データ読み取り開始"); const resultB = await readData(); console.log("B: データ読み取り完了", resultB); });
// 書き込み処理C(共有ロックが解放されるまで待機) navigator.locks.request("data", { mode: "exclusive" }, async () => { console.log("C: データ書き込み開始"); await writeData(); console.log("C: データ書き込み完了"); });
このコードでは、AとBが共に"data"に対する共有ロックを取得するため、両者は並行して読み取りを行います。一方Cは排他ロックを要求するので、AおよびBが完了して共有ロックがすべて解放されるまで待ってから開始します。実行順序として、AとBはほぼ同時に開始し、終了→その後Cが開始→終了、となります。
このように、共有ロックを使うと「同時に実行しても問題ない処理」を並列化でき、パフォーマンス向上につながります。特に読み取り系処理は並列実行して、書き込み系のみ単独実行する、という使い分けが簡単に実現できます。
ifAvailableオプションの使用例:ロック未取得時のみ処理を実行するサンプルコード
ifAvailableオプションの使い所を示す例です。このオプションを使うと、ロックがすぐ取れない場合には処理を諦めたりスキップしたりできます。
シナリオとして、「定期実行タスクがあるが、もし前回のタスクがまだ終わっていなければ新しいタスクは実行しない」という場合を考えます。
function tryTask() { navigator.locks.request("task", { ifAvailable: true }, (lock) => { if (!lock) { console.log("前回タスク実行中のため今回スキップ"); return; } // ロック取得成功時のみ以下実行 console.log("タスク開始"); performTask(); // 非同期でなければこのまま呼ぶ console.log("タスク終了"); }); }
このtryTask関数を例えば1分おきに呼ぶように設定しておくと、前回のperformTask()が長引いている場合にはlockがnullとなり、「スキップ」のログが出ます。逆に空いていればロック取得に成功し、タスクが実行されます。
重要なのはif (!lock)のチェックで、これによりロックが取れなかった(すなわち他で保持されていた)ことを検知しています。ifAvailableを使う際は、このlock判定を忘れないようにしましょう。ここで早期リターンすることで、実際には何もせずに関数を終わらせています。
このパターンは、周期的なジョブやリソース消費の大きな処理の重複実行を避けたい場合に有用です。「一度に一つだけ実行」という制約を緩やかに守りつつ、タイミングが重なったら後発をキャンセルすることでシステムを守ります。
LockManager.query活用例:現在のロック状態を確認する方法とその出力
navigator.locks.query()メソッドを使うと、現在そのオリジンで保持されているロックや待機中のロック要求の情報を取得できます。これは主にデバッグ目的ですが、使いこなすと便利です。
ロック状態をログに出力する簡単な例を示します。
async function logLocks() { const state = await navigator.locks.query(); console.log("現在保持中のロック:", state.held); console.log("待機中のロック要求:", state.pending); }
state.heldには現在取得されているロックの配列が、state.pendingには待機キューに入っているリクエストの配列が入っています。それぞれの要素にはname(ロック名)やmode(exclusive/shared)、clientId(ロック保持者の識別子)などの情報が含まれます。これを見れば、「今どのロックが取られていて、いくつ待っているか」が一目瞭然です。
例えば、何らかの理由でロックが長時間保持されたままになってしまった場合(通常は自動解放されますが、デッドロックなどで可能性あり)、logLocks()を呼んでheldを確認することで原因追及ができます。また、複数タブで複雑にやり取りするアプリでは、開発中に状態をログ出力して追跡するとバグを潰しやすくなるでしょう。
なお、query()メソッドはあくまで情報を取得するだけでロックには影響しません。安全に呼び出せますが、頻繁に呼ぶと些細ながら性能に影響するかもしれないので、必要なときだけにしましょう。
stealオプションの利用例:既存ロックを奪取して新しいロックを取得するサンプルコード
stealオプションの実際の使い方を見てみます。これは強力なため慎重に扱うべきですが、ここでは理解のためにあえて使用した例を示します。
シナリオとして、ある処理Aがロックを保持して長時間実行中に、緊急の処理Bがやってきたとします。BはAに割り込んででも実行したい、という場合にstealを使います。
// A: 長時間処理(既に実行中と仮定) navigator.locks.request("resource", { mode: "exclusive" }, async () => { console.log("A処理: 開始"); await longTask(); // 長時間かかる処理 console.log("A処理: 終了"); });
// B: 緊急処理(Aに割り込む) navigator.locks.request("resource", { mode: "exclusive", steal: true }, async () => { console.log("B処理: 開始(steal成功)"); await urgentTask(); console.log("B処理: 終了"); });
この場合、Aが"resource"の排他ロックを保持中に、Bがsteal: trueでロックを要求します。すると、Aのロックは強制的に解放され、Bが即座にロックを取得して実行を開始します。Aはどうなるかというと、ロックが無くなった状態で処理を続行することになります。A自身はその事実を知りませんが、以後Aの排他性は保証されません(実際この例ではBが割り込んで並行実行されてしまっています)。ログ出力順で見ると、A処理: 開始→B処理: 開始→B処理: 終了→A処理: 終了となり、Bが途中で入り込んでいるのがわかります。
ご覧のように、stealを使うと排他の前提が崩れるため、非常に危険です。AとBの処理内容によってはデータ整合性が大きく損なわれるでしょう。そのため現実のアプリケーションでstealを使う際は、「割り込まれても問題が少ない処理」に限るとか、「B終了後にAを改めて最初から実行しなおす」などのフォローが必要です。基本的には使わないことが望ましいですが、例えばユーザーの強制キャンセル操作に対応するためにstealで長時間タスクを打ち切る、といった特殊なケースでのみ検討してください。
複数ロック取得時の注意例:デッドロックを避けるための順序制御テクニック
最後に、複数のロックを扱う場合のコーディングパターンを紹介します。先に説明したデッドロックの回避策として、ロック取得順序を統一する方法があります。
例として、リソース"A"と"B"の2つのロックがあり、ある処理では両方を取得して実行する必要があるとします。悪い例は以下のようなものです。
// 悪い例:異なる場所で異なる順序のロック取得 navigator.locks.request("A", async () => { await navigator.locks.request("B", async () => { // A→Bの順で取得 doWorkForAB(); }); });
navigator.locks.request("B", async () => { await navigator.locks.request("A", async () => { // B→Aの順で取得(デッドロックの危険) doWorkForBA(); }); });
このように、あるところではA→Bの順、別のところではB→Aの順でロックを取ろうとすると、両者が同時に走った場合にデッドロックとなります。そこで、以下のように順序を統一します。
// 良い例:常にA→Bの順序でロック取得 async function withLocksAB(callback) { await navigator.locks.request("A", async () => { await navigator.locks.request("B", async () => { await callback(); }); }); }
// 使い方 withLocksAB(async () => { doWorkForAB(); }); withLocksAB(async () => { doWorkForBA(); });
このようにwithLocksABというユーティリティ関数を作り、常にA→Bの順でロックを取得するようにします。これを経由して処理を実行すれば、順序が乱れることはありません。もちろんシナリオによって順序を決めづらい場合もありますが、その場合はそもそも設計を見なおすべきかもしれません。
また、どうしても動的な組み合わせで複数ロックを扱う場合は、ネストではなくPromise.allで並列にnavigator.locks.requestを呼ぶテクニックもあります。ただし内部的な順序は結局管理されるため、確実とは言えません。最もシンプルなのは上述のように順序ルールを決めておくことです。
デッドロックは発生してからでは遅いため、コードを書く段階でこのようなテクニックを用いて予防することが大切です。
Web Locks API利用で遭遇しがちなエラーとその対策:事例から学ぶトラブルシューティング方法
ここでは、Web Locks APIを利用する中で開発者が陥りがちなミスや直面しうるエラー、それに対する対策をいくつか紹介します。正しく実装したつもりでも、思わぬところでハマるポイントがあるかもしれません。事前に知っておけば迅速に対処できるでしょう。
Web Locks API未対応の環境(代表例はInternet Explorer)では、navigator.locksが存在しません。このような場合、コード上で例えばnavigator.locks.request(...)を呼ぶとTypeErrorになるでしょう。これを避けるためには、事前に'locks' in navigatorで存在チェックを行うことが第一です。前述のコード例でも示したように、
if (!('locks' in navigator)) { // フォールバック処理 }
というガードを入れておけば安全です。
フォールバック処理の内容はアプリに依存しますが、未対応の場合は機能を無効化するか、ごく簡単な代替実装を入れることになります。例えば同期機能なら「未対応ブラウザでは自動同期せず手動更新のみ」とするなどの対応も考えられます。重要なのは、エラーのまま放置しないことです。チェックを怠ると、そもそも古い環境でアプリが動かなくなり、ユーザーに何も伝わらず終わってしまいます。検出してコンソールに警告を出すだけでも、開発・サポート時には有益です。
なお、navigator.locksが存在しない環境としては、IE以外にもファイル://プロトコル(セキュアコンテキストでない)で開いたページなども該当します。ローカルHTMLファイルを直接開くような使われ方を想定するなら、その点も留意しましょう。
ロックが取得できずタスクが停止する場合:その原因と解決策を探る
ロック待ちが発生した際、本来なら待機して順番が来れば動くはずですが、場合によってはタスクが停止したように見えることがあります。これは概ね以下のような原因が考えられます。
- 別のコンテキストがロックを保持したまま解放しない(デッドロックやバグ)。
- ロック待ちしている処理自身が何らかの理由でキャンセルされてしまった(タブが閉じられた等)。
- 単に待ち時間が長く、人間から見て停止しているように感じる。
最初のケースは典型的なデッドロック状態です。例えば、異なるロック間で互いに待っている場合など、システムが行き詰まってしまうため、いつまで経ってもロックが取得できません。この場合の解決策はデッドロックの解消です。前述した順序の統一や、AbortSignalによるタイムアウトを設けることで、一定時間で諦めて抜ける処理を入れるのが効果的です。仮にデッドロックが発生しても、自動で抜けるようにしておけば完全停止は避けられます。
二つ目のケースでは、例えばユーザーがタブを閉じてしまったために、そのコンテキストの待ち処理が途中で消滅した場合などが考えられます。基本的にタブを閉じればそのロック待ちはキャンセルされますが、他方で残った処理がそれを知らず待ち続けていると認識齟齬が起きる可能性があります。ただ、Web Locks APIの待ち行列管理はブラウザ側で行われるので、タブが閉じれば自動的にキューから取り除かれるため、このケースでシステム全体が止まることは通常ありません。
三つ目の「単に長いだけ」は、システムに問題はないもののUXとして問題になるケースです。この場合は進捗表示を行うなど、ユーザーに待っていることを知らせるのが親切です。もしくは許容できない待ち時間であれば、システム設計的にロック粒度を見直すことも検討します。
いずれにしても、「ロックが取得できない=何かがおかしい」と感じたら、LockManager.query()で状態を確認したり、デッドロックの有無を疑ったりして原因を突き止めることが重要です。
ifAvailable指定時にコールバックが呼ばれない:発生条件と対処のポイント
ifAvailableを指定した場合、ロックが取れないときにはコールバックが呼ばれないと誤解してしまうかもしれません。しかし、実際にはifAvailableでもコールバック自体は必ず呼ばれます(ロックオブジェクトがnullで渡されるだけ)。したがって、「コールバックが実行されない」という事象は本来起きません。
それでも「コールバックが呼ばれていないようだ」と見える場合、多くはコールバック関数内でlockのチェックを怠り、早々にreturnしてしまっているケースです。例えば先ほどのtryTask例ではif (!lock) return;としていました。このreturnはtryTaskから抜けているだけで、コールバック関数自体は実行されています。ただ、処理内容が空だったために何も起こらず、呼ばれなかったように見えただけです。
従って、ifAvailable使用時のポイントは、lockがnullの場合に分岐する処理を適切に書くことです。そこを誤ると、スキップしたつもりがなく単に沈黙したように映ります。対処としては、nullの場合にログ出力や代替処理をきちんと行うことで、動作を把握しやすくなります。「呼ばれない」と思ったら、まずそのコールバック内にログを仕込んでみて、本当に一度も入っていないのか確認するのも良いでしょう。
steal使用時に想定外のロック解放が起こる:競合発生時の注意点と対処策
stealオプション使用時には、既存ロックが強制解放されるため、開発者にとって「想定外」に見える動作を招きます。先述の例でも示した通り、実行中の処理から突然ロックが奪われるため、通常は起こり得ない並行実行が発生します。これによりデータ競合や不整合が生じるリスクが高まります。
この問題への基本的な対処策は、stealを可能な限り使わないことです。どうしても必要な場合でも、奪われた側の処理に影響が出ないか慎重に検証する必要があります。例えば、stealで割り込まれうる処理では、重要なクリティカルセクションは別途保護する、あるいは処理が中断されても再実行して整合性を取る、といった対応が考えられます。しかし非常に高度な制御になるため、避けるに越したことはありません。
また、stealを使う際はユーザーにも注意を促すのが親切です。例えば「緊急停止機能」のようにUI上に現れる機能であれば、「現在進行中の処理を中断して新しい処理を開始します」といった警告を出すことで、何が起こるかを明示できます。そうでなく内部的に自動でstealする場合でも、ログなどで分かるようにしておきましょう。
まとめると、steal使用時の想定外動作は、注意深い設計とユーザーへの配慮でカバーするしかありません。原則的には使わない方向で検討し、使うならば副作用を十分把握した上で導入することが肝心です。
ロック処理内で同期処理を実行するとUIフリーズ:問題の原因と回避策
Web Locks APIそのもののエラーではありませんが、ロック処理内で重い同期処理(非asyncの処理)を行うと、UIがフリーズすることがあります。これはJavaScriptのシングルスレッド特性によるもので、特にメインスレッドで実行されるコードが長時間CPUを占有すると、ブラウザが他のレンダリングやイベント処理を行えなくなるためです。
Web Locks APIのコールバックは通常メインスレッド上で実行されるため、もしその中で大きな計算や無限ループなどを実行してしまうと、ユーザーから見ると画面が固まったように感じられます。これは排他制御とは直接関係ありませんが、「ロックを取っている間は安全だ」と思って重たい処理を入れてしまうと起こりがちな問題です。
回避策としては、以下のような方法があります。
- 処理を分割する: 重い処理は小分けにして、
setTimeoutやrequestAnimationFrameで少しずつ実行することでメインスレッドを占有しすぎないようにします。 - Web Workerを利用する: 計算量の多いタスクはWeb Workerにオフロードし、メインスレッドではワーカからの結果を待つだけにします。
- UI更新を考慮: ロック中でも適宜UIを更新したりプログレス表示をすることで、ユーザーにフリーズではなく処理中であると認識してもらいます。
根本的には、Web Locks API使用時に限らず、メインスレッドで同期的に長時間ブロックするコードを書かないことが重要です。ロックを取得しているときにUIを更新する必要が生じた場合も、できれば一旦ロックを解放してからUI操作し、またロックを取り直すなどの検討もできます(ただしその間に状況が変わらないよう注意)。
まとめると、「ロック中だからと言って安心して重たい処理を詰め込まない」ことがポイントです。排他制御とUI応答性は別問題なので、両者をバランスよく満たすよう設計しましょう。
他の排他制御手法との比較:Web Locks APIと従来のブラウザ上のロック機構との違いを徹底比較
最後に、Web Locks APIとその他の排他制御手法を比較し、その違いを確認します。ブラウザ環境・Webアプリ開発において、Web Locks API以外にも考えられる競合制御のアプローチはいくつかあります。ここでは代表的なものと比較することで、Web Locks APIの位置づけを明確にしましょう。
ローカルストレージやCookieを用いた簡易ロック機構との比較:実装の手間と信頼性の違い
Web Locks API登場前によく取られていた手段の一つが、localStorageやCookieを使った簡易ロックもどきの実装です。例えばlocalStorageにlock_acquired=trueのような値を書き込み、それを他のタブが監視して「誰かがロックしている」とみなす方法があります。またCookieにフラグを設定して同様の判断をすることも可能です。
これらの方法は簡単に実装できますが、信頼性の面でWeb Locks APIには及びません。localStorageは更新通知(storageイベント)こそありますが、原子操作ではないためタイミング次第では複数タブが同時に「ロック取得」と見なしてしまう競合が起こりえます。また、仮に厳密に排他できたとしても、ロック開放忘れ(例えばタブがクラッシュしてロックフラグが残留)した場合に永遠に他が進まなくなる問題もあります。開発者がタイマーで監視してフラグをクリアする、といった冗長な仕組みを追加する必要があり、実装も煩雑です。
それに比べてWeb Locks APIは、ブラウザが排他を厳密に管理し、待ち行列も面倒を見てくれます。ロックは自動解放され、タブ終了時もクリーンアップされるため、開発者が管理する状態はありません。実装の手間と信頼性の両面で、Web Locks APIはlocalStorage/Cookie方式より優れています。強いて言えば、前者は独自実装ゆえに任意のポリシー(例えばタイムアウトなど)を組み込みやすい柔軟性がありますが、同等のことはWeb Locks APIでもAbortSignalやstealで実現できるため、大きな優位とは言えません。
Service Workerを介した排他制御との比較:単一プロセス調停方式との違いと課題
Service Workerはオリジンごとに一つだけ存在する特殊なワーカーで、複数タブの共通処理を担います。そのため、ある意味Service Workerを「一種のロック保持者」として使い、すべてのクリティカルな処理をService Worker経由で行うという設計も考えられます。例えば、複数タブがデータ保存を要求したら、一旦Service Workerにメッセージを送り、Service Workerが順番に処理する、といった具合です。
このアプローチは単一のスレッド(Service Worker)に仕事を集中させることで排他制御を実現します。確かに、同時に2つの処理が走ることは避けられますが、実装の手間はかなりのものです。ポストメッセージで通信を行い、結果を受け取ってまた各タブに返信する、といったコードを書く必要があります。また、Service Worker自体にも同時に複数のイベント(例えばpushと同期要求など)が来れば並行処理される可能性があり、完全に一度に一つとは限りません。結局、Service Worker内でも何らかのキューイングを自前で実装する必要が出てきます。
それに対しWeb Locks APIは、各タブから直接排他要求ができ、ブラウザがそれを調停します。メッセージングのコードを書く必要もなく、より直観的です。強いて欠点を言えば、Service Worker方式では処理の集中管理がしやすい(ログを一箇所に集約できる等)という点はありますが、Web Locks APIでもLockManager.query()で全体像は掴めます。総合的に見ると、Web Locks APIの方がシンプルで実装ミスも起こりにくいため、有利でしょう。
BroadcastChannelなどタブ間通信手法との比較:リーダー選出パターンとの関係性
複数タブ間で協調する方法として、BroadcastChannelによる直接通信や、SharedWorkerを使ったタブ間共有も考えられます。これらを使うと、タブ同士でメッセージをやり取りして「あなたがやって」「わかりました」と役割分担を決める、すなわちリーダー選出を実現できます。
例えば、最初に開いたタブが「自分がリーダーです」とBroadcastChannelで宣言し、他のタブはそれを聞いて「では自分は従属します」と決めることで、一つのタブだけがデータ同期を行う、ということが可能です。実際、このようなパターンはWebLocksがない時代に採用されることがありました。
しかし、この方法も実装が煩雑で、またエッジケースの処理(リーダータブが閉じたら次を誰にするか等)に気を遣う必要があります。その点Web Locks APIは、暗黙的に「最初にロックを取れたやつがリーダー」のような動きをしますし、リーダーがいなくなれば次が自動で繰り上がります(キュー内の次のリクエストが実行される)。つまり、リーダー選出と役割の引き継ぎを自動でやってくれると見ることもできます。
BroadcastChannel方式は、細かく制御したい場合には柔軟ですが、そうでなければWeb Locks APIに任せたほうが断然シンプルです。特に、ロック取得失敗時の待ち処理やタイムアウトといった部分を自前で書かなくてよいのは大きな利点です。総じて、特殊な要件がない限り、タブ間通信よりWeb Locks APIの方が開発効率は高いでしょう。
Web Worker内のAtomic操作との比較:スレッド間同期手法との適用範囲の差異
Web環境で他に排他制御と言えば、SharedArrayBuffer + Atomicsを使った低レベル同期があります。これはJavaScriptに組み込まれた非常に低水準のスレッド間同期原語で、Web Worker間で共有メモリを使ってミューテックスやセマフォを実現することも可能です。
しかし、Atomic操作は主に計算処理やデータ処理の微細な同期に使われ、タブ間(異なるメインスレッド間)には適用できません。SharedArrayBufferは同一ページ内のWorkerとの間でしか共有できず、ブラウザ全体でのグローバルな同期には不向きです。また、Atomic操作は難解でバグを生みやすく、一般的なウェブ開発者にはハードルが高いものです。
それに対しWeb Locks APIは、高レベルで抽象化された操作であり、使いやすさも段違いです。コンテキストの違いも意識する必要がなく、「同じオリジン内なら全部カバーする」ことができます。言ってみれば、Atomic+共有メモリで作ったユーザ空間ロックと、OSが提供するファイルロックほどの差があります。Web Locks APIはブラウザというOSが提供するロック機構であり、Atomicはユーザ空間で苦労して実装するロックというイメージです。
特殊なケースを除き、わざわざAtomic操作で独自ロックを組む必要性は低いでしょう。特にAtomicは他タブに効かない時点で、マルチタブ問題の解決策にはなりえません。やはり、マルチスレッドやデータ並行処理にAtomicを使い、それとは別にWeb Locks APIでタブ間の調整をするという具合に、レイヤを分けて活用するのが現実的です。
サーバーサイドロックとの比較:クライアント主導のロックとの利点・欠点の対比
最後に、Web Locks APIのようなクライアントサイドのロックと、サーバーサイドで実装するロック機構を比較します。サーバーサイドロックとは、データベースのロックや分散ロックサービスなど、複数クライアント間で共有リソースを調整する仕組みのことです。
例えば、オンライン協調編集ではサーバー側で編集トークンを管理し、一度に一人しか編集できないよう制御したりします。また、在庫処理ではDBの行ロックで同時購入をさばいたりもします。こうしたアプローチは、ユーザー間(つまりオリジンを超えたグローバルな範囲)での競合を防ぐには必須です。ただし、クライアント同士(同ユーザーの複数タブ)の競合まで全てサーバーで面倒見るのは効率が悪かったり、細かな挙動を制御しづらかったりします。
Web Locks APIはあくまで一つのブラウザ内(単一ユーザー)での問題解決にフォーカスしています。そのため、マルチユーザー間の同期には使えませんが、逆に言えば単一ユーザー内のことをサーバーに持ち込まずクライアント内で解決できるメリットがあります。不要なサーバー通信を減らせ、クライアント側の自律性が上がります。
欠点としては、サーバー視点では各クライアントが何をやっているか把握できないため、本当にそれで全競合が解決しているか保証が持てない点です。最終的な整合性はサーバー側でも検証すべきでしょう(例えば注文重複チェックなど)。
要するに、Web Locks APIとサーバーサイドロックは対立するものではなく、補完関係にあります。グローバルな競合はサーバーで制御しつつ、ローカルな競合はクライアント内で解決することで、二段構えの安全策とするのが理想です。Web Locks API導入後も、サーバー側の整合性チェックやロックは省略せず、多重防御の一環と考えると良いでしょう。