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

· Webentwicklung  · 7 minuten Lesezeit

Instagram Karussell vollständig erfassen mit MutationObserver

Instagram lädt Karussell-Slides lazy. Nur die ersten zwei sind beim Öffnen im DOM. Ein MutationObserver und eine while-Schleife lesen alle Slides automatisch aus, ohne einen einzigen Classnamen anzufassen.

Instagram lädt Karussell-Slides lazy. Nur die ersten zwei sind beim Öffnen im DOM. Ein MutationObserver und eine while-Schleife lesen alle Slides automatisch aus, ohne einen einzigen Classnamen anzufassen.

Inhalt

Zwei Slides. Sechs im Post.

Die Chrome Extension erfasst Instagram-Posts und speichert sie mit ihren Metadaten in einer Vektordatenbank. Zu den Metadaten gehört auch der Alt-Text der Bilder. Instagram generiert diese Alt-Texte automatisch und beschreibt den Bildinhalt auf Englisch. Für ein RAG-System sind das wertvolle semantische Informationen. Wie das Embedding aus diesen Texten erzeugt wird, beschreibe ich im Artikel über den Qdrant-Aufbau und das Embedding-System.

Bei einem Einzelbild ist das kein Problem. Du erfasst, ein <img> ist im DOM, der Alt-Text steht drin.

Bei einem Karussell-Post sieht das anders aus.

Instagram rendert Karussell-Posts so, dass initial nur zwei Slides im DOM landen. Der erste ist sichtbar, der zweite ist bereits geladen und wartet hinter dem Bildbereich. Slide 3, 4, 5 sind überhaupt nicht im DOM. Sie werden erst geladen, wenn der Nutzer auf den Weiter-Button klickt.

Wenn du also querySelectorAll('img[alt]') auf einem Karussell-Post ausführst, bekommst du im besten Fall zwei Alt-Texte. Auch wenn der Post sechs Slides hat.

Die DOM-Struktur des Karussells

Bevor ich den Algorithmus erkläre, lohnt sich ein Blick auf die eigentliche Struktur.

Die Slides stecken in einer <ul>. Jeder Slide ist ein <li>-Element. Der aktive Slide hat transform: translateX(0px) in seinem Inline-Style, der nächste bereits geladene Slide hat einen positiven Pixel-Wert. Das erste <li> in der Liste ist ein unsichtbarer Spacer ohne Inhalt.

<ul>
  <li style="transform: translateX(2807px); width: 1px;"></li>
  <!-- Spacer -->
  <li tabindex="-1" style="transform: translateX(0px);">
    <!-- Slide 1: visible -->
    <div>

      <img alt="Photo by The Stoics on May 19, 2026. May be an image of text…" />
    </div>
  </li>
  <li tabindex="-1" style="transform: translateX(468px);">
    <!-- Slide 2: already in DOM, not yet visible -->
    <div>

      <img alt="Photo by The Stoics on May 19, 2026. May be an image of text…" />
    </div>
  </li>
  <!-- Slide 3–N: not yet in DOM -->
</ul>

Zwei Navigations-Buttons leben außerhalb der <ul>, aber innerhalb desselben Post-Containers:

<button aria-label="Zurück">…</button> <button aria-label="Weiter">…</button>

Der Weiter-Button verschwindet aus dem DOM, wenn der letzte Slide aktiv ist. Das ist die natürliche Abbruchbedingung für den Algorithmus.

DOM-Struktur eines Instagram-Karussells mit ul, li-Slides und Weiter-Button

Abbildung: DOM-Ausschnitt eines Karussell-Posts: die ul-Container mit li-Slides, Inline-Transform-Styles und den Navigations-Buttons ausserhalb der Liste.

Das Bild zeigt den DOM-Ausschnitt eines Karussell-Posts im Browser: die äußere <ul> mit dem unsichtbaren Spacer-<li> ganz links, dann die ersten zwei Slides mit ihren transform: translateX(…)-Inline-Styles, und die Navigations-Buttons außerhalb der Liste. Der Rest der Slides ist schlicht nicht vorhanden. Sie existieren im DOM nicht, bevor der Nutzer klickt.

Die <ul> enthält initial nur zwei Slides. Slide 3 bis N werden erst dynamisch eingefügt, wenn der Nutzer den Weiter-Button klickt. Das img-Tag mit dem alt-Attribut ist im jeweiligen <li> vollständig vorhanden, sobald es in den DOM eingefügt wird.

Das bekannte Muster mit MutationObserver

Ich hatte ein ähnliches Problem schon bei der Caption-Expansion gelöst. Instagram kürzt lange Captions ab und blendet einen Mehr-Button ein. Die Extension klickt diesen Button und wartet mit einem MutationObserver, bis der vollständige Text im DOM erscheint. Der Artikel dazu ist hier zu finden.

Das Grundprinzip ist übertragbar: Programmatisch klicken, dann auf eine DOM-Veränderung warten statt blind zu pollen.

Beim Karussell klicke ich den Weiter-Button und beobachte die <ul> auf neue <li>-Elemente. Sobald ein neues <li> mit einem <img> erscheint, lese ich den Alt-Text aus und klicke weiter.

Den Observer vor dem Klick starten

Das ist der entscheidende Punkt, und der Fehler, den man intuitiv macht, ist falsch.

Wenn du zuerst klickst und dann den Observer startest, kannst du die Mutation verpassen. Instagram ist schnell. In seltenen Fällen, zum Beispiel auf einem System mit schnellem Netz oder bei bereits im Cache liegendem Inhalt, kann die Mutation in dem Moment passieren, bevor dein Observer überhaupt registriert wurde.

Die korrekte Reihenfolge:

// 1. Observer starten
const waitPromise = waitForNewCarouselSlide(ul, initialCount);

// 2. Only then click
button.click();

// 3. Wait for the observer
await waitPromise;

Der Observer ist registriert, bevor der Klick ausgelöst wird. Eine Race Condition ist damit strukturell ausgeschlossen.

Der Timeout-Fallback für vorgeladene Slides

Slide 2 ist bereits im DOM, bevor wir auch nur einmal klicken. Wenn wir von Slide 1 auf Slide 2 wechseln, wird kein neues <li> in die <ul> eingefügt. Instagram ändert nur die Transform-Werte der bestehenden Elemente.

Der Observer wartet auf ein neues <li>. Das kommt nicht. Er würde ewig warten.

Die Lösung ist ein Timeout-Fallback:

const waitForNewCarouselSlide = (ul: HTMLElement, initialCount: number): Promise<void> =>
  new Promise((resolve) => {
    let settled = false;
    const finish = () => {
      if (settled) return;
      settled = true;
      observer.disconnect();
      resolve();
    };

    const observer = new MutationObserver(() => {
      const currentCount = ul.querySelectorAll('li img[alt]').length;
      if (currentCount > initialCount) finish();
    });

    observer.observe(ul, { childList: true, subtree: true });
    setTimeout(finish, 1000); // Fallback nach 1 Sekunde
  });

Passiert beim Übergang von Slide 1 auf Slide 2: Der Observer beobachtet, nichts ändert sich an der Anzahl der Slides, der Timeout feuert nach 1 Sekunde und löst die Promise auf. Bei allen weiteren Slides, die wirklich neu geladen werden, feuert der Observer sofort, weit vor dem Timeout.

Das Timing-Verhalten in der Praxis:

ÜbergangNeuer Slide im DOM?Wie resolvet die Promise
Slide 1 → 2Nein (vorgeladen)Timeout nach 1 s
Slide 2 → 3JaObserver, sofort
Slide N → N+1JaObserver, sofort
Letzter Slidekeinewhile-Schleife endet

Ein 6-Slide-Karussell dauert damit insgesamt etwa 1,2 Sekunden.

Die Schleife

Der vollständige Algorithmus läuft in einer while-Schleife. Die Abbruchbedingung ist das Verschwinden des Weiter-Buttons aus dem DOM.

const extractAllAltTexts = async (postRoot: HTMLElement): Promise<string[]> => {
  const nextButton = findCarouselNextButton(postRoot);

  // No next button: not a carousel, direct fallback
  if (!nextButton) return extractImageAltTexts(postRoot);

  const ul = findCarouselList(postRoot);
  if (!ul) return extractImageAltTexts(postRoot);

  const allAltTexts = new Set<string>();

  // Collect initially loaded slides (1 and 2)
  collectAltTextsFromCarouselList(ul, allAltTexts);

  let button: HTMLElement | null = nextButton;
  while (button) {
    const initialCount = ul.querySelectorAll('li img[alt]').length;

    // Start observer BEFORE clicking
    const waitPromise = waitForNewCarouselSlide(ul, initialCount);
    button.click();
    await waitPromise;

    collectAltTextsFromCarouselList(ul, allAltTexts);

    // Re-find next button — absent on the last slide
    button = findCarouselNextButton(postRoot);
  }

  return Array.from(allAltTexts);
};

Ein Set<string> übernimmt die Deduplizierung. Slide 1 und 2 sind vor der Schleife bereits erfasst. Die Schleife fügt nur neue Alt-Texte hinzu. Wenn der Weiter-Button fehlt, ist findCarouselNextButton null, die Schleife endet.

Ablaufdiagramm: Karussell-Traversal mit Observer, Klick und Set-Akkumulierung

Abbildung: Ablauf des Karussell-Traversals: Observer starten, Weiter-Button klicken, auf neues li warten, Alt-Text sammeln, bis kein Weiter-Button mehr vorhanden ist.

Das Diagramm zeigt die Schleife von der initialen Erfassung bis zum Ende. Jeder Durchlauf startet den Observer, klickt, wartet und sammelt. Der Weiter-Button-Check entscheidet, ob die Schleife weiterläuft oder endet.

Den Weiter-Button semantisch finden

Auch hier gilt dasselbe Prinzip wie bei der Caption-Expansion: kein Classname. Der Weiter-Button hat aria-label="Weiter", auf englischsprachigen Instagram-Instanzen aria-label="Next".

const NEXT_BUTTON_LABELS = ['Weiter', 'Next'];

const findCarouselNextButton = (postRoot: HTMLElement): HTMLElement | null => {
  for (const label of NEXT_BUTTON_LABELS) {
    const btn = postRoot.querySelector<HTMLElement>(`button[aria-label="${label}"]`);
    if (btn) return btn;
  }
  return null;
};

Die aria-label-Attribute gehören zur Accessibility-Infrastruktur von Instagram. Sie sind deutlich stabiler als Classnames, die sich bei jedem Deploy ändern können.

Was das für das RAG-System bedeutet

Der Alt-Text jedes einzelnen Slides wird jetzt Teil des Embeddings. Für einen 6-Slide-Post mit einer Motivations-Spruch-Serie enthält das Embedding damit alle sechs Sprüche, nicht nur den ersten.

Das verbessert die Retrieval-Qualität direkt. Eine semantische Suchanfrage nach einem Inhalt, der auf Slide 4 steht, findet den Post jetzt zuverlässig. Vorher blieb dieser Inhalt für das Vektorsystem unsichtbar.

Der Overhead ist minimal: 1 Sekunde für den ersten Slide-Übergang, dann jeweils unter 200 Millisekunden für alle weiteren. Die gesamte Karussell-Navigation läuft im Hintergrund, nachdem der Nutzer den Screenshot im Overlay bestätigt hat.


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: Artikel lesen
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen
  14. Instagram Karussell vollständig erfassen mit MutationObserver (dieser Artikel)
  15. Notiz und Tags beim Screenshot-Speichern: Artikel lesen
  16. Instagram Tastatur-Shortcuts blockieren Chrome Extension Eingaben: Artikel lesen
  17. Lowercase-Normalisierung und Duplikat-Erkennung im Tag-Input: Artikel lesen
  18. Zitadel Login V2 in Docker Compose: drei versteckte Fehler: Artikel lesen
  19. PKCE OAuth in einer Chrome MV3 Extension: Artikel lesen
  20. React Frontend mit react-oidc-context und Zitadel: Artikel lesen
  21. Vite Build-Time-Umgebungsvariablen in Docker: Artikel lesen

Du baust ein Datenerfassungssystem auf Basis von Browser Extensions mit komplexen DOM-Interaktionen? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen