Web Locks API für Tab-Koordination: Race Conditions vermeiden
AI generated
JS
() =>
JavaScript · Browser APIs · Nebenläufigkeit
Web Locks API für Tab-Koordination
Race Conditions zwischen Browser-Tabs vermeiden

Öffnet ein Nutzer dieselbe Webanwendung in mehreren Tabs, laufen viele Hintergrundaufgaben unbemerkt parallel: Token-Refresh, Cache-Aufbau, Synchronisation mit dem Server. Die Web Locks API stellt mit navigator.locks eine exklusive Sperre über Origin-Grenzen zwischen Tabs bereit, mit der solche Aufgaben koordiniert und doppelte Ausführung zuverlässig verhindert werden.

17 Min. Lesezeit navigator.locks.request · ifAvailable · steal Chrome · Edge · Firefox · Safari 16.4+

1. Warum Tabs miteinander koordiniert werden müssen

Moderne Single-Page-Anwendungen laufen häufig in mehreren Tabs gleichzeitig, ein Nutzer öffnet dieselbe Anwendung in einem zweiten Fenster, um parallel zu arbeiten. Ohne Koordination führt das zu klassischen Race Conditions: Zwei Tabs erkennen gleichzeitig, dass das Access-Token abgelaufen ist, und feuern beide einen Refresh-Request. Der Server invalidiert unter Umständen das erste Refresh-Token, sobald das zweite eintrifft, und ein Tab landet mit einem ungültigen Token im ausgeloggten Zustand. Die Web Locks API wurde genau für dieses Problem entwickelt.

Mit navigator.locks stellt die Web Locks API ein origin-weites Sperrsystem bereit, das über alle offenen Tabs, Web Worker und Service Worker derselben Origin hinweg funktioniert. Anders als localStorage-Events oder BroadcastChannel-Nachrichten, die nur Informationen austauschen, erzwingt die Web Locks API echte Exklusivität: Nur ein Ausführungskontext erhält gleichzeitig eine benannte Sperre, alle anderen warten oder werden je nach Modus sofort abgelehnt.

Typische Einsatzfälle der Web Locks API sind Token-Refresh, das einmalige Initialisieren eines IndexedDB-Schemas, das Verhindern doppelter Analytics-Events beim Tab-Start und die Wahl eines "Leader-Tabs" für Aufgaben, die nur einmal ausgeführt werden sollen, etwa eine WebSocket-Verbindung, die von allen Tabs gemeinsam genutzt wird.

2. Grundlagen: navigator.locks.request im Detail

Der zentrale Einstiegspunkt der Web Locks API ist navigator.locks.request(name, callback). Der Name identifiziert die Sperre eindeutig innerhalb der Origin, der Callback erhält ein Lock-Objekt und wird erst ausgeführt, sobald die Sperre erteilt wurde. Entscheidend: Die Sperre wird automatisch freigegeben, sobald das vom Callback zurückgegebene Promise erfüllt oder abgelehnt wird, es gibt keine explizite unlock()-Methode, die vergessen werden könnte.

Dieses Design der Web Locks API verhindert eine ganze Klasse von Bugs, die von klassischen Locking-APIs bekannt sind: vergessene Freigaben, die zu dauerhaft blockierten Ressourcen führen. Der Rückgabewert von request() ist selbst ein Promise, das mit dem Rückgabewert des Callbacks aufgelöst wird, sodass sich Ergebnisse problemlos aus dem gesperrten Bereich herausreichen lassen.


// Basic exclusive lock across all tabs of the same origin
async function withExclusiveLock(name, task) {
  return navigator.locks.request(name, async (lock) => {
    console.log(`Lock "${lock.name}" acquired in mode ${lock.mode}`);
    const result = await task();
    // No manual unlock needed — releases automatically when the
    // callback's promise settles, even if task() throws.
    return result;
  });
}

const total = await withExclusiveLock("cart-total", async () => {
  return computeCartTotal();
});

3. Praxisbeispiel: Token-Refresh ohne Duplikate

Das klassische Beispiel für die Web Locks API ist der Token-Refresh in Anwendungen mit mehreren offenen Tabs. Ohne Koordination erkennt jeder Tab unabhängig, dass das Token abläuft, und stellt eigenständig einen Refresh-Request. Mit navigator.locks.request("token-refresh", ...) erhält nur ein Tab die Sperre, führt den Refresh durch, speichert das neue Token in localStorage oder IndexedDB, und alle anderen Tabs warten, bis die Sperre freigegeben wird, um anschließend direkt das bereits aktualisierte Token zu lesen.

Wichtig bei diesem Muster der Web Locks API ist, nach dem Erhalt der Sperre erneut zu prüfen, ob das Token tatsächlich noch abgelaufen ist. Ein wartender Tab könnte die Sperre erst erhalten, nachdem ein anderer Tab den Refresh bereits erledigt hat, ein erneuter Refresh wäre dann unnötig und potenziell schädlich, falls der Server Refresh-Token nach einmaliger Nutzung invalidiert.


// Token refresh coordinated across tabs with the Web Locks API
async function getValidAccessToken() {
  let token = readTokenFromStorage();
  if (!isExpired(token)) return token;

  return navigator.locks.request("token-refresh", async () => {
    // Re-check: another tab may have refreshed while we were waiting
    token = readTokenFromStorage();
    if (!isExpired(token)) return token;

    const response = await fetch("/api/token/refresh", {
      method: "POST",
      credentials: "include",
    });
    const fresh = await response.json();
    writeTokenToStorage(fresh);
    return fresh.accessToken;
  });
}

4. Singleton-Tab-Muster mit exklusiven Sperren

Ein weiteres starkes Muster der Web Locks API ist die Wahl eines Leader-Tabs. Manche Aufgaben, etwa eine dauerhafte WebSocket-Verbindung für Live-Updates, sollen nur von genau einem Tab unterhalten werden, während alle anderen Tabs die empfangenen Daten über BroadcastChannel weiterleiten bekommen. Der Trick: Ein Tab fordert eine Sperre an, die nie freigegeben wird, solange der Tab lebt, das zugehörige Promise löst also niemals auf.

Alle anderen Tabs versuchen ebenfalls, dieselbe Sperre zu erhalten, bleiben aber in der Warteschlange, bis der aktuelle Leader-Tab geschlossen wird, wodurch der Browser die Sperre automatisch freigibt. Der nächste Tab in der Warteschlange übernimmt dann automatisch die Leader-Rolle. Dieses Muster der Web Locks API braucht keine manuelle Heartbeat-Logik, weil der Browser das Freigeben bei Tab-Schließung oder Absturz garantiert übernimmt.


// Leader election: exactly one tab owns the shared WebSocket connection
let isLeader = false;

navigator.locks.request("websocket-leader", { mode: "exclusive" }, () => {
  isLeader = true;
  const socket = new WebSocket("wss://api.mironsoft.de/live");

  socket.addEventListener("message", (event) => {
    // Fan out updates to every open tab through BroadcastChannel
    leaderChannel.postMessage(JSON.parse(event.data));
  });

  // Never resolve: hold the lock as long as this tab is the leader.
  // Releasing happens automatically when the tab closes or crashes.
  return new Promise(() => {});
});

5. Warteschlangen und faire Reihenfolge

Fordern mehrere Tabs gleichzeitig dieselbe Sperre an, bildet die Web Locks API intern eine Warteschlange nach First-In-First-Out-Prinzip. Jede Anfrage wird in der Reihenfolge bearbeitet, in der sie eingegangen ist, was vorhersehbares Verhalten garantiert, ganz ohne dass ein Tab durch wiederholtes Anfragen bevorzugt behandelt wird. Diese Fairness-Garantie ist besonders wichtig bei Aufgaben wie sequentiellen Schreibvorgängen in IndexedDB, bei denen die Reihenfolge der Operationen ein Ergebnis beeinflusst.

Zusätzlich unterstützt die Web Locks API geteilte Sperren über den Modus { mode: "shared" }. Mehrere Leser können eine geteilte Sperre gleichzeitig halten, während ein exklusiver Schreibzugriff warten muss, bis alle Leser fertig sind, klassisches Reader-Writer-Locking, wie man es aus Datenbanksystemen kennt, jetzt direkt im Browser verfügbar.

6. Nicht blockierende Prüfung mit ifAvailable

Nicht jeder Anwendungsfall der Web Locks API soll warten, bis eine Sperre frei wird. Die Option { ifAvailable: true } lässt den Callback sofort mit lock === null aufrufen, wenn die Sperre bereits von einem anderen Kontext gehalten wird, statt in die Warteschlange einzureihen. Das eignet sich für Aufgaben, die ohnehin nur "best effort" ausgeführt werden müssen, etwa ein periodischer Cache-Refresh, der einfach beim nächsten Intervall erneut versucht wird, falls gerade ein anderer Tab denselben Cache aktualisiert.

Die Option { steal: true } geht den entgegengesetzten Weg und bricht eine bestehende Sperre sofort ab, um sie an den neuen Anfragenden zu übergeben. Das ist mit Vorsicht zu verwenden, weil der ursprüngliche Halter der Sperre keine Chance hat, sauber abzuschließen, es eignet sich vor allem für Recovery-Szenarien, bei denen ein hängender Tab eine Sperre nie wieder freigeben würde.


// Non-blocking check: skip work if another tab already holds the lock
async function refreshCacheIfNoOneElseIs() {
  await navigator.locks.request(
    "cache-refresh",
    { ifAvailable: true },
    async (lock) => {
      if (lock === null) {
        console.log("Another tab is already refreshing the cache, skipping");
        return;
      }
      await rebuildLocalCache();
    }
  );
}

7. Lebenszyklus, Timeouts und AbortController

Weil navigator.locks.request() ohne zusätzliche Optionen unbegrenzt lange warten kann, ist die Integration eines AbortController für produktive Anwendungen der Web Locks API Pflicht. Über die Option signal lässt sich ein Timeout realisieren: Läuft die Zeit ab, bevor die Sperre erteilt wurde, bricht die Anfrage mit einem AbortError ab, statt den Nutzer unbegrenzt warten zu lassen.

Ein weiterer wichtiger Aspekt: Sperren der Web Locks API sind an den Lebenszyklus des Ausführungskontexts gebunden. Schließt ein Nutzer einen Tab, der gerade eine Sperre hält, gibt der Browser die Sperre garantiert frei, unabhängig davon, ob der zugehörige JavaScript-Code sauber beendet wurde. Das unterscheidet sich fundamental von serverseitigen Locking-Mechanismen, bei denen ein abgestürzter Prozess ein Lease-Timeout benötigt, um die Sperre wieder freizugeben.


// Abort a lock request after a timeout to avoid indefinite waiting
async function requestLockWithTimeout(name, task, timeoutMs = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await navigator.locks.request(
      name,
      { signal: controller.signal },
      task
    );
  } catch (err) {
    if (err.name === "AbortError") {
      console.warn(`Lock "${name}" not acquired within ${timeoutMs}ms`);
      return null;
    }
    throw err;
  } finally {
    clearTimeout(timer);
  }
}

8. Debugging: Sperren in Chrome DevTools sichtbar machen

Chrome DevTools bietet unter Application, Locks eine dedizierte Ansicht, die alle aktiven Sperren der Web Locks API mit Name, Modus und beteiligtem Client anzeigt. Diese Ansicht ist unverzichtbar, um Deadlocks oder unerwartet lange Wartezeiten zu diagnostizieren, etwa wenn ein Tab abstürzt, bevor er neu geladen wurde, und DevTools zeigt sofort, ob die Sperre trotzdem korrekt freigegeben wurde.

Ergänzend liefert navigator.locks.query() denselben Zustand programmatisch als Objekt mit held- und pending-Arrays. Das eignet sich, um in Monitoring-Dashboards oder Debug-Overlays den aktuellen Sperrzustand der Web Locks API zur Laufzeit anzuzeigen, ohne DevTools öffnen zu müssen, etwa während automatisierter End-to-End-Tests, die Race Conditions gezielt provozieren wollen.

9. Web Locks API im Vergleich zu Alternativen

Vor der Web Locks API wurden Tab-übergreifende Koordinationsprobleme mit Workarounds gelöst, die entweder unzuverlässig oder deutlich komplexer waren. Der direkte Vergleich zeigt, warum die native API in den meisten Fällen die bessere Wahl ist.

Ansatz Mechanismus Exklusivität garantiert? Nachteil
localStorage-Flag Boolean-Wert manuell setzen Nein, race-anfällig Kein atomarer Test-and-Set
BroadcastChannel-Abstimmung Nachrichten austauschen, Timeout abwarten Nein, nur best effort Komplexe Timing-Logik nötig
Server-seitiges Locking Redis-Lock oder DB-Transaktion Ja Netzwerk-Roundtrip, Server-Abhängigkeit
Web Locks API navigator.locks.request() Ja, browserseitig garantiert Nur innerhalb derselben Origin

Die Web Locks API ersetzt server-seitiges Locking nicht in jedem Fall, sie ist explizit auf Koordination innerhalb einer Origin im Browser beschränkt. Für rein clientseitige Probleme wie Token-Refresh, Leader-Wahl oder IndexedDB-Initialisierung ist sie aber die deutlich einfachere und robustere Lösung, ohne zusätzlichen Netzwerk-Roundtrip und ohne eigene Timeout-Logik für abgestürzte Clients.

Mironsoft

JavaScript-Entwicklung, Browser APIs und moderne Web-Anwendungen

Doppelte Requests durch mehrere Tabs im Griff?

Wir integrieren die Web Locks API in eure Anwendung, für zuverlässigen Token-Refresh, Leader-Wahl zwischen Tabs und race-freie IndexedDB-Zugriffe.

Tab-Koordination

navigator.locks für Token-Refresh, Caching und Leader-Wahl korrekt eingesetzt

Robustheit

Timeouts, AbortController und Fallback für Browser ohne Unterstützung

Debugging-Setup

Sperrzustand über navigator.locks.query() in Monitoring integriert

10. Zusammenfassung

Die Web Locks API löst ein Problem, das in Anwendungen mit mehreren offenen Tabs schnell zu subtilen Bugs führt: konkurrierende Aktionen, die sich gegenseitig ins Gehege kommen. Mit navigator.locks.request() lässt sich Token-Refresh koordinieren, ein Leader-Tab für gemeinsam genutzte Ressourcen wählen und der Zugriff auf IndexedDB serialisieren, ganz ohne Server-Roundtrip und ohne manuelle Freigabe-Logik.

Wer die Web Locks API produktiv einsetzt, sollte immer einen Timeout über AbortController einplanen, nach Erhalt der Sperre den Ausgangszustand erneut prüfen, und die Chrome-DevTools-Ansicht unter Application, Locks zur Diagnose nutzen. Die automatische Freigabe bei Tab-Schließung macht die API robuster als die meisten selbstgebauten Locking-Mechanismen und verdient einen festen Platz im Werkzeugkasten für Multi-Tab-Anwendungen.

Web Locks API für Tab-Koordination — Das Wichtigste auf einen Blick

Exklusive Sperren

navigator.locks.request(name, callback) gewährt nur einem Tab gleichzeitig Zugriff auf eine benannte Ressource.

Automatische Freigabe

Keine manuelle unlock-Methode. Die Sperre löst sich, sobald das Callback-Promise erledigt ist oder der Tab schließt.

Modi

ifAvailable für nicht blockierende Prüfungen, steal für Recovery, shared für Reader-Writer-Muster.

Diagnose

Chrome DevTools unter Application, Locks sowie navigator.locks.query() zeigen den aktuellen Sperrzustand.

11. FAQ: Web Locks API für Tab-Koordination

1Was ist die Web Locks API?
Exklusive oder geteilte Sperren über alle Tabs derselben Origin via navigator.locks.request(), um Race Conditions zu verhindern.
2Manuelle Freigabe nötig?
Nein, automatisch beim Erledigen des Callback-Promise oder beim Schließen des Tabs.
3Über mehrere Origins hinweg nutzbar?
Nein, nur innerhalb derselben Origin. Für andere Domains braucht es serverseitiges Locking.
4Tab mit Sperre stürzt ab?
Der Browser gibt die Sperre garantiert frei, unabhängig vom sauberen Beenden des Codes.
5Unbegrenztes Warten verhindern?
Mit signal und AbortController einen Timeout setzen, der die Anfrage mit AbortError abbricht.
6Was macht ifAvailable?
Callback erhält sofort lock gleich null statt zu warten, wenn die Sperre bereits belegt ist.
7Wofür eignet sich steal?
Für Recovery-Szenarien, in denen eine bestehende Sperre nie freigegeben würde.
8Leader-Tab wählen?
Sperre mit niemals auflösendem Promise anfordern, Warteschlange übernimmt beim Schließen automatisch.
9Sperren debuggen?
Chrome DevTools, Application, Locks oder programmatisch navigator.locks.query().
10Browser-Unterstützung?
Chrome, Edge, Firefox vollständig, Safari ab 16.4. Feature-Detection mit 'locks' in navigator.