Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· Webentwicklung  · 6 minuten Lesezeit

Race Condition in Chrome Extensions debuggen und vermeiden

Das Shadow DOM Overlay löste eine Race Condition aus. Instagram wurde als generic erkannt, alle Metadaten fehlten. Ursache und Fix mit capturedElement und einem previewOpen Flag.

Das Shadow DOM Overlay löste eine Race Condition aus. Instagram wurde als generic erkannt, alle Metadaten fehlten. Ursache und Fix mit capturedElement und einem previewOpen Flag.

Inhalt

Der Fehler, der sich nicht reproduzieren lässt

Ich hatte Phase 2 der Extension fertiggestellt. Tailwind CSS v4 im Popup, Shadow DOM Overlay für die Vorschau, farbkodierte Duplikatserkennung beim Hover. Alles gebaut, alles getestet. Dann kam das erste echte Nutzungsfeedback.

Die Plattform wurde nicht mehr erkannt. Jeder Capture auf Instagram lieferte platform: "generic". Keine Metadaten. Kein Channel-Name. Kein Timestamp. Nur die URL und der Seitentitel.

Das war unerwartet, weil die Plattformerkennung überhaupt nichts geändert hatte. Kein Commit hatte payload-context-builder.ts angefasst. Die Logik war identisch.

Und genau das ist das Symptom einer Race Condition: Der Fehler liegt nicht im geänderten Code. Er liegt in der zeitlichen Beziehung zwischen zwei Vorgängen, die vorher nie ein Problem waren.

Wie die Plattformerkennung funktioniert

Um zu verstehen, warum der Fehler passiert, muss man wissen, wie die Extension die Metadaten extrahiert. Der Ablauf über die drei Kontexte ist:

  1. Background empfängt capture-screenshot
  2. Background fragt beim Content Script: get-element-bounds → bekommt die Position des gerade gehoverten Elements
  3. Background macht den Screenshot via captureVisibleTab
  4. Content Script schneidet das Bild zu
  5. Content Script zeigt das Preview Overlay und wartet
  6. User klickt Bestätigen
  7. Background fragt beim Content Script: get-instagram-metadata → Content Script extrahiert Metadaten aus dem gespeicherten highlightedElement

Die entscheidende Variable ist highlightedElement. Sie wird im Content Script bei jedem mouseover-Event aktualisiert. Sie zeigt immer auf das Element, über dem die Maus gerade ist.

Warum das Overlay das Problem verursacht

Schritt 6 ist der Moment, an dem die Race Condition entsteht.

Der User klickt auf den Bestätigen-Button im Shadow DOM Overlay. Das Overlay wird aus dem DOM entfernt: host.remove(). Das ist ein DOM-Mutation-Event. Der Browser reagiert darauf und feuert ein mouseover-Event auf das Element, das sich jetzt physisch unter dem Cursor befindet, da das Overlay weggefallen ist.

Das Content Script hört auf alle mouseover-Events. Es aktualisiert highlightedElement sofort.

Dann, Millisekunden später, schickt der Background Service Worker die Anfrage get-instagram-metadata. Das Content Script verarbeitet sie und liest highlightedElement. Aber das zeigt nicht mehr auf den Instagram-Post. Es zeigt auf das Element, das nach dem Entfernen des Overlays unter dem Cursor lag. Oft ein Button, ein Container, ein Navigations-Element.

// content/main.ts -> the problem in one variable
let highlightedElement: HTMLElement | null = null;

document.addEventListener('mouseover', (event) => {
  if (!isScreenshotActive) return;
  highlightedElement = event.target as HTMLElement; // ← gets overwritten after overlay dismiss
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.action === 'get-instagram-metadata') {
    extractInstagramPostMetadata(highlightedElement) // ← reads the already overwritten value
      .then((metadata) => sendResponse({ metadata }));
    return true;
  }
});

Die Extraktion schlägt fehl, weil highlightedElement kein Instagram-Post mehr ist. Sie gibt null zurück. Der Background fällt auf generic zurück. Keine Metadaten.

Zeitstrahl der Race Condition durch Overlay-Dismiss Abbildung: Zeitstrahl der Race Condition. Der User drückt Ctrl+Q, das Overlay öffnet sich, der User klickt Bestätigen, das DOM-Remove-Event feuert ein neues mouseover, und erst dann kommt die get-instagram-metadata Anfrage an, aber highlightedElement zeigt schon auf ein anderes Element.

Die Lösung in zwei Teilen

Das Problem hat zwei Ursachen, also braucht es zwei Gegenmaßnahmen.

Teil 1: Element beim richtigen Zeitpunkt einfrieren

Der richtige Zeitpunkt ist get-element-bounds. Das ist die Nachricht, die der Background am Anfang des Capture-Flows schickt. In diesem Moment gilt: Der User hat gerade Ctrl+Q gedrückt. Das highlightedElement ist genau das, was er erfassen will.

Ich friere es als capturedElement ein:

let capturedElement: HTMLElement | null = null;

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message.action === 'get-element-bounds') {
    capturedElement = highlightedElement; // ← snapshot at the right moment
    const bounds = getElementBounds();
    sendResponse({ bounds });
    return false;
  }

  if (message.action === 'get-instagram-metadata') {
    const elementToExtract = capturedElement || highlightedElement; // ← stable reference
    extractInstagramPostMetadata(elementToExtract).then((metadata) => sendResponse({ metadata }));
    return true;
  }
});

capturedElement wird nicht durch mouseover verändert. Es bleibt stabil für den gesamten Capture-Flow, unabhängig davon, was der Browser dazwischen feuert.

Teil 2: Mouseover während des Overlays pausieren

Das Einfrieren der Referenz ist der wichtigste Fix. Aber es gibt noch eine zweite Schwachstelle: Wenn der User die Maus auf dem Weg zum Bestätigen-Button über Instagram-Elemente bewegt, schreibt das mouseover-Listener weiterhin in highlightedElement. Das ist unnötig und verursacht visuelle Artefakte (die Umrandungsfarbe springt).

Die Lösung ist ein previewOpen-Flag:

let previewOpen = false;

// In the show-preview handler
if (message.action === 'show-preview') {
  previewOpen = true;
  showCapturePreview(message.dataUrl, message.width, message.height).then((confirmed) => {
    previewOpen = false; // ← only release when overlay is gone
    sendResponse({ confirmed });
  });
  return true;
}

// In the mouseover listener
document.addEventListener('mouseover', (event) => {
  if (!isScreenshotActive || previewOpen) return; // ← no update while overlay is open
  highlightedElement = event.target as HTMLElement;
});

Solange das Overlay offen ist, werden keine neuen Hover-Events verarbeitet. Die Maus kann sich beliebig bewegen. highlightedElement bleibt stabil. Und capturedElement ist sowieso eingefroren.

Zwei-Variablen-Lösung für stabiles Element-Tracking im Capture-Flow Abbildung: Der Fix mit zwei Variablen. capturedElement wird beim get-element-bounds eingefroren und nicht mehr durch mouseover überschrieben. previewOpen blockiert den mouseover-Listener für die Dauer des Overlays.

Was dieser Fehler über Chrome Extension Architektur lehrt

Das Interessante an dieser Race Condition ist, dass sie nicht durch schlechten Code entstand. Die ursprüngliche Architektur war für den ursprünglichen Ablauf korrekt.

Das Problem entstand durch die zeitliche Entkopplung in einem dreistufigen Nachrichtensystem. Wenn A eine Nachricht an B schickt und B mit C interagiert bevor A die nächste Nachricht schickt, kann der Zustand in B zwischen den Nachrichten von A durch C verändert werden.

Das ist keine Chrome-Extension-Besonderheit. Das ist das fundamentale Problem aller event-driven Systeme mit geteiltem Zustand. Die Lösung ist immer dieselbe: Zustand zum richtigen Zeitpunkt einfrieren und explizit durch den Flow tragen, statt ihn als globale Variable immer neu zu lesen.

In diesem Fall war der richtige Zeitpunkt get-element-bounds. Ab da ist die Absicht des Users klar. Ab da darf sich nichts mehr am Ziel des Captures ändern.

Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: Artikel lesen
  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection: Artikel lesen
  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht (dieser Artikel)
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen

Du arbeitest an einer Chrome Extension oder einem anderen event-driven System und stolperst über ähnliche Zustandsprobleme? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen