# React Log Viewer: 100.000 Zeilen streamen mit Regex-Suche

> Bau einen React Log Viewer, der 100.000 Zeilen streamt: Virtualisierung, Level-Filter, Regex-Suche und ein Follow-Modus, der beim Hochscrollen sofort pausiert.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 16. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 8 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-log-viewer
- Englische Fassung: https://wingo-ui.com/blog/react-log-viewer

## Kurz gesagt

Ein React Log Viewer, der einen echten Stream übersteht, braucht vier Dinge: Virtualisierung, damit von 100.000 Zeilen nur die sichtbaren im DOM stehen, Appends, die pro Frame in einem einzigen Render landen, Level- und Regex-Filter, die das Tippen nicht blockieren, und einen Follow-Modus, der stoppt, sobald jemand hochscrollt. Der Log Viewer von Wingo UI bringt alle vier mit, nimmt Zeilen aus einer EventSource oder einem WebSocket über setApi().append entgegen und hält standardmäßig einen Ringpuffer von 100.000 Zeilen.

Du hast einen Deploy-Job, einen Sync-Worker oder einen KI-Agenten, der Zeilen ausgibt, und willst sie live im Browser sehen. Die erste Version ist ein `<pre>` mit `lines.map()`. Bei 200 Zeilen klappt das. Bei 20.000 ruckelt der Tab, die Ansicht reißt dich bei jeder neuen Zeile nach unten, während du gerade einen Fehler liest, und die Suche rendert bei jedem Tastendruck alles neu. Dieser Guide zeigt, was ein React Log Viewer können muss, damit er unter einem Live-Stream schnell und lesbar bleibt: Virtualisierung, gebündelte Appends, Level- und Regex-Filter und ein Follow-Modus, der weiß, wann er loslassen muss. Die Beispiele nutzen den [Log Viewer](https://wingo-ui.com/components/log-viewer) von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Die Techniken funktionieren aber in jeder Umsetzung.

## Was muss ein React Log Viewer können?

Er muss mit Menge, Tempo, Speicher, Suche, Mitlaufen und Terminal-Ausgabe klarkommen. Denn ein Log ist eine Liste, an die nur angehängt wird, die ohne Ende wächst und die man überfliegt, durchsucht und rückwärts liest:

- **Menge.** Zehntausende Zeilen sind bei einem Build normal, also müssen die Zeilen virtualisiert werden.
- **Tempo.** Bei viel Betrieb schickt ein Stream Dutzende Zeilen pro Sekunde. Ein Render pro Animation Frame reicht.
- **Speicher.** Ein Job, der stundenlang läuft, braucht einen Ringpuffer, der die ältesten Zeilen verwirft und das auch anzeigt.
- **Finden.** Level-Chips mit Zählern, eine Textsuche, die Groß- und Kleinschreibung ignoriert, und ein Regex-Log-Filter für Muster wie Fehlercodes.
- **Mitlaufen.** Neue Zeilen scrollen ins Bild, solange du das Ende beobachtest, und stoppen sofort, wenn du hochscrollst.
- **Terminal-Ausgabe.** Build-Tools geben ANSI-Farbcodes aus. Render sie oder entfern sie, aber zeig nie `\u001b[32m` als Text.

Die meisten Beispiele, die Logs live in React anzeigen, hören beim `<pre>` auf. Für ein Debug-Panel mit ein paar hundert Zeilen reicht das. Ein Streaming Log Viewer, den jemand eine Stunde lang offen lässt, braucht alle sechs Punkte.

## Wie rendere ich 100.000 Log-Zeilen, ohne dass die Seite einfriert?

Halt die Zeilen aus dem React-State heraus und render nur die Zeilen, die gerade auf dem Bildschirm sind. Mehr braucht es nicht.

Wenn du bei jeder Nachricht ein `useState`-Array per Spread kopierst, kopierst du alle Zeilen und renderst die Elternkomponente neu, und zwar für jede einzelne Zeile. Der Log Viewer hält seine Zeilen stattdessen in einem internen Store. Du schiebst Zeilen über `setApi().append(...)` hinein, und alle Appends, die im selben Animation Frame ankommen, landen gemeinsam in einem einzigen Render. Deine eigene Komponente rendert bei einer neuen Zeile nie neu.

Fürs DOM nutzt der Viewer `@tanstack/react-virtual`: Als Elemente existieren nur die Zeilen im Viewport plus 20 Zeilen Overscan auf jeder Seite. Umbrochene Zeilen bekommen zuerst eine Höhe, die aus Textlänge und Panelbreite geschätzt wird, und nach dem Render eine echte Messung. Liest du nach oben, springt die Ansicht deshalb kaum.

Installier ihn und schließ ihn an einen Stream an:

```bash
npx wingo-ui@latest add log-viewer
```

```tsx
"use client"

import { useEffect, useState } from "react"
import { LogViewer, type LogViewerApi } from "@/components/ui/log-viewer"

export function JobLog({ jobId }: { jobId: string }) {
  const [api, setApi] = useState<LogViewerApi | null>(null)

  useEffect(() => {
    if (!api) return
    const source = new EventSource(`/api/jobs/${jobId}/logs`)
    // jede Nachricht ist eine Zeile: reiner Text oder ein JSON-Log-Eintrag
    source.onmessage = (event) => api.append(event.data)
    return () => source.close()
  }, [api, jobId])

  return <LogViewer setApi={setApi} fileName={`job-${jobId}`} searchShortcut="mod+shift+f" />
}
```

`append` nimmt ein Zeilenobjekt, einen String oder ein Array aus beidem. Strings laufen durch `parseLogLine`. Die Funktion versteht Zeilen wie `2026-09-24T16:38:12.345Z INFO [sync] message` oder `[16:38:12] WARN message` und JSON-Zeilen. Bei JSON sucht sie die Nachricht in `msg`, `message` oder `text`, das Level in `level`, `lvl`, `severity` oder `levelname` (inklusive der numerischen Levels von pino) und die Zeit in `time`, `timestamp`, `ts` oder `@timestamp`. Eine JSON-Zeile ohne Nachrichten-Key erscheint als Rohtext. Alle weiteren Keys landen in `meta` und erscheinen per Klick oder Enter als JSON-Baum unter der Zeile, abzüglich `pid`, `hostname` und `v` von pino. Ein WebSocket funktioniert genauso. Hast du die Zeilen schon im State, gib sie stattdessen als `lines` weiter: Bei einem längeren Array hängt der Viewer nur das neue Ende an.

### Wie sieht die Serverseite aus?

Server-Sent Events sind der einfachste Transport in eine Richtung, und `EventSource` verbindet sich von selbst neu. `subscribeToJobLogs` steht für deine eigene Queue oder deinen eigenen Log-Tail:

```ts
// app/api/jobs/[id]/logs/route.ts
import { subscribeToJobLogs } from "@/lib/job-logs" // dein eigenes Pub/Sub: gibt eine Unsubscribe-Funktion zurück

export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const encoder = new TextEncoder()
  let unsubscribe = () => {}

  const stream = new ReadableStream({
    start(controller) {
      unsubscribe = subscribeToJobLogs(id, (record) => {
        // ein JSON-Eintrag pro Event, damit ein Zeilenumbruch in einer Nachricht ihn nicht zerteilt
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(record)}\n\n`))
      })
    },
    cancel() {
      unsubscribe()
    },
  })

  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
  })
}
```

Damit nach einem Reconnect keine Zeilen doppelt ankommen, gib jedem Event ein `id:`-Feld: Der Browser schickt die letzte ID im Header `Last-Event-ID` zurück. Probier den Viewer unten aus und scroll hoch, während er streamt.

> Live-Demo (Log Viewer): Das Job-Log streamt 20 Zeilen pro Sekunde. Scroll hoch: Das Mitlaufen pausiert, ein Button zählt die neuen Zeilen, und ein Tippen darauf oder die Taste Ende bringt dich zurück zur neuesten Zeile. Füg 10.000 Zeilen hinzu und sieh zu, wie der ganze Schub in einem Frame landet. Probier sie unter [Log Viewer](https://wingo-ui.com/components/log-viewer) aus und installier sie mit `npx wingo-ui@latest add log-viewer`.

## Woher weiß der Follow-Modus, wann er stoppen soll?

Der Follow-Modus sollte beim ersten Zeichen stoppen, dass jemand nach oben will, noch bevor sich die Scrollposition bewegt hat. Die meisten selbstgebauten Versionen prüfen nach jedem Scroll-Event, ob die Ansicht höchstens N Pixel vom Ende entfernt ist. Bei einem schnellen Stream kommt zwischen der Mausraddrehung und diesem Scroll-Event eine neue Zeile an, und die Ansicht springt wieder nach unten, bevor sie sich überhaupt bewegt hat.

Der Log Viewer folgt vier Regeln:

1. **Eine Mausraddrehung nach oben pausiert sofort,** beim `wheel`-Event (`deltaY < 0`), bevor sich `scrollTop` geändert hat.
2. **Jedes Scrollen nach oben pausiert ebenfalls, solange der Inhalt nicht geschrumpft ist.** Das deckt Touch, die Scrollbar und Tasten ab. Die Höhenprüfung ist wichtig: Schrumpft der Inhalt (alte Zeilen entfernt, ein Filter greift), verkleinert der Browser `scrollTop` von selbst, ohne dass die Person etwas getan hat.
3. **Scrolls, die der Code selbst auslöst, werden ignoriert.** Korrigiert der Virtualizer `scrollTop`, nachdem er eine umbrochene Zeile über der Ansicht gemessen hat, lösen Scroll-Events in den nächsten 80ms weder eine Pause noch ein Fortsetzen aus. Auf iOS kommt diese Korrektur erst, wenn der Schwung einer Wischgeste ausgelaufen ist, und sähe sonst aus, als hätte jemand gescrollt.
4. **Das Mitlaufen setzt erst bei einem klaren Signal wieder ein:** zurückscrollen bis auf 4px vor dem Ende, die Taste Ende, der Follow-Schalter oder der Button „12 new lines“.

Das Scrollen selbst kommt aus dem Hook [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom). Er gleitet in einer `requestAnimationFrame`-Schleife sanft nach unten, liest das Ziel in jedem Frame neu, damit er auch Inhalt erwischt, der während der Animation wächst, und rendert nur neu, wenn sich seine Flags `isAtBottom` oder `isFollowing` ändern. Während der Pause zeigt der Footer „Paused“ statt „Live“, und wenn alte Zeilen aus dem Ringpuffer fallen, bleibt die Zeile, die du gerade liest, an ihrem Platz. Auch das Öffnen der JSON-Details einer Zeile pausiert.

Steuer das Mitlaufen mit `follow` / `onFollowChange`, oder setz `defaultFollow={false}` für ein abgeschlossenes Log, das oben beginnen soll.

### Kann ich den Hook allein nutzen?

Ja, für ein kurzes Log oder einen Chat, bei dem Virtualisierung übertrieben wäre. Setz `scrollRef` auf den Scroll-Container und `contentRef` auf das Element, das wächst, und zeig einen Button, solange die Person nicht am Ende ist. Der Hook zählt alles innerhalb von 72px vor dem Ende als „unten“ und folgt wieder, wenn die Person in diesen Bereich zurückscrollt. Mit `threshold` änderst du den Wert, mit `initial: false` startet die Ansicht oben:

```tsx
"use client"

import { useStickToBottom } from "@/hooks/use-stick-to-bottom"
import { Button } from "@/components/ui/button"

export function BuildOutput({ lines }: { lines: string[] }) {
  const { scrollRef, contentRef, isAtBottom, scrollToBottom } = useStickToBottom()

  return (
    <div className="relative h-80">
      <div ref={scrollRef} className="h-full overflow-y-auto overscroll-contain rounded-2xl bg-foreground/5">
        <pre ref={contentRef} className="p-4 font-mono text-xs leading-6">
          {lines.join("\n")}
        </pre>
      </div>
      {!isAtBottom && (
        <Button size="sm" radius="full" className="absolute bottom-3 left-1/2 -translate-x-1/2" onClick={() => scrollToBottom()}>
          Zur neuesten Zeile
        </Button>
      )}
    </div>
  )
}
```

Ab ein paar tausend Zeilen wechselst du zum vollen Viewer. Für lange Listen, die keine Logs sind (Kontakte, Feeds), bietet dir die [Virtual List](https://wingo-ui.com/components/virtual-list) dasselbe Windowing mit Sticky-Gruppenüberschriften und Infinite Loading. Die Chat-Variante dieses Scroll-Problems steht in [unserem Guide zur React AI Chat UI](https://wingo-ui.com/de/blog/react-ai-chat-ui), das Streaming von Text in [React Markdown Streaming ohne Scroll-Sprünge](https://wingo-ui.com/de/blog/react-markdown-streaming).

## Wie sollten Level-Filter und Regex-Suche funktionieren?

Levels gehören in Toggle-Chips mit Live-Zählern, Text in eine Suche mit Debounce, die auf einen regulären Ausdruck umschalten kann. Zeilen ohne Level bleiben immer sichtbar. Steuer die Chips mit `levelFilter` und `onLevelFilterChange`, wenn die Elternkomponente den State hält, zum Beispiel um ihn mit der URL zu synchronisieren:

```tsx
"use client"

import { useState } from "react"
import { LogViewer, type LogLevel, type LogLine } from "@/components/ui/log-viewer"

export function IncidentLog({ lines }: { lines: LogLine[] }) {
  const [levels, setLevels] = useState<LogLevel[]>(["warn", "error", "fatal"])

  return (
    <LogViewer
      lines={lines}
      levelFilter={levels}
      onLevelFilterChange={setLevels}
      filterMode="highlight"
      regex
      defaultQuery={String.raw`BR-\w+-\d+`}
      defaultFollow={false}
      height={360}
    />
  )
}
```

Die Textsuche findet Wörter in der Nachricht oder der Quelle, ohne auf Groß- und Kleinschreibung oder diakritische Zeichen zu achten. Der Schalter `.*` (oder die Prop `regex`) wechselt zu einem regulären Ausdruck, ebenfalls ohne Groß- und Kleinschreibung. Drei Details machen das brauchbar:

- **Debounce.** Der Filter läuft 120ms nach dem letzten Tastendruck, nicht bei jeder Taste.
- **Ungültige Muster scheitern sanft.** `new RegExp` wirft bei Eingaben wie `(`. Der Viewer markiert das Feld als ungültig und filtert nichts, statt jede Zeile auszublenden.
- **Große Puffer filtern in Blöcken.** Über 50.000 Zeilen prüft er jeweils 8.000 Zeilen in Idle-Callbacks, damit das Tippen flüssig bleibt.

Wenn du Logs mit Regex filtern willst und den Filter selbst schreibst, verzichte bei `test()` auf das Flag `g`: Eine globale Regex merkt sich `lastIndex` zwischen den Aufrufen und überspringt Treffer. Ein Muster mit verschachtelten Quantoren wie `(a+)+$` kann bei manchen Eingaben außerdem sehr lange backtracken, und dagegen hilft kein Debounce.

`filterMode="filter"` blendet die Zeilen aus, die nicht passen. `highlight` behält jede Zeile, markiert die Treffer und springt mit Enter und Shift+Enter zwischen ihnen. Das ist besser, wenn die Zeilen rund um einen Fehler wichtig sind.

> Live-Demo (Log Viewer): Highlight-Modus mit Regex: Jeder BR-CO- und BR-US-Code ist markiert, und der Rest des Logs bleibt, wo er ist. Drück in der Suche Enter für den nächsten Treffer, Shift+Enter für den vorherigen. Probier sie unter [Log Viewer](https://wingo-ui.com/components/log-viewer) aus und installier sie mit `npx wingo-ui@latest add log-viewer`.

## Ist ein Log Viewer mit Tastatur und Screenreader bedienbar?

Ja, wenn der Log-Bereich ein einziger Tab-Stopp ist statt 100.000 fokussierbarer Zeilen. Im Log Viewer ist er ein Tab-Stopp mit `role="log"`. Darin verschieben Pfeil hoch und Pfeil runter eine Fokuszeile, Bild auf und Bild ab springen um eine Seite, Pos1 und Ende gehen zur ersten und zur neuesten Zeile, und Escape hebt die Fokuszeile wieder auf. Strg+F oder ⌘F im Viewer springt in seine Suche; `searchShortcut` ergänzt ein globales Tastenkürzel.

Eine Live-Region, die jede gestreamte Zeile vorliest, wäre unbenutzbar. Das Log ist deshalb `aria-live="off"`, und eine eigene Region mit der Stufe „polite“ sagt die Fokuszeile an, wenn sie sich bewegt. Alle Texte stehen zum Übersetzen in `labels`.

## Wie funktioniert ein Log Viewer auf dem Smartphone?

Er bricht Zeilen um und räumt zweitrangige Bedienelemente aus dem Weg. Mit `defaultWrap="auto"` (Standardwert) brechen Zeilen unter 768px um und scrollen darüber seitlich. Ist der Viewer schmaler als 480px, wandern die Schalter für Umbruch und Zeit ins Menü ⋯, die Level-Chips scrollen seitlich, und unter 768px öffnet sich das Menü als Bottom Sheet. Das Menü kopiert außerdem die sichtbaren Zeilen, lädt den ganzen Puffer als datierte `.log`-Datei herunter oder leert den Puffer. Zeilen mit mehr als 10.000 Zeichen werden gekürzt und bekommen einen Button „Show all“, und `maxLines` begrenzt den Speicher. Für Jobs, die tagelang laufen, setzt du den Wert niedriger.

## Soll ich einen Log Viewer von npm nehmen oder den Quellcode selbst besitzen?

Suchst du einen Log Viewer für React, entscheidet vor allem eine Frage: Wem gehört der Code? Willst du ein npm-Paket, ist [@melloware/react-logviewer](https://github.com/melloware/react-logviewer) der gepflegte Fork von Mozillas react-lazylog. Stand Oktober 2026 ist die neueste Version auf [npm](https://www.npmjs.com/package/@melloware/react-logviewer) 6.6.0 (veröffentlicht am 27. September 2026), unter der Lizenz MPL-2.0. Das Paket lädt Text aus einer URL, einem WebSocket oder einer EventSource, rendert ANSI-Farben, sucht, virtualisiert mit Virtua und folgt dem Ende über den Wrapper `ScrollFollow`. Es ist die bessere Wahl, wenn du vor allem ganze Log-Dateien von einer URL öffnest und Updates über npm willst.

Der Viewer von Wingo UI ist eine Quelldatei in deinem Projekt. Zeilen-Markup, Parser und Levels kannst du also selbst ändern. Er ist eine Pro-Komponente: Stand Oktober 2026 kostet Pro $8 im Monat, $80 im Jahr oder $150 einmalig, nachzulesen auf [der Preisseite von Wingo UI](https://wingo-ui.com/de/pricing).

## Wie probiere ich ihn am schnellsten aus?

Installier den Viewer, richte eine `EventSource` auf den Log-Endpunkt deines Jobs und gib `setApi` weiter:

```bash
npx wingo-ui@latest add log-viewer
```

Der Befehl fügt auch die Abhängigkeiten hinzu, darunter `useStickToBottom` und den [JSON Viewer](https://wingo-ui.com/components/json-viewer) für die Details einer Zeile. Tragen deine Log-Einträge große Payloads, geht [unser Guide zum React JSON Viewer](https://wingo-ui.com/de/blog/react-json-viewer) auf diesen Baum im Detail ein. Weitere Guides wie diesen findest du in [der Kategorie Komponenten-Guides](https://wingo-ui.com/de/blog/category/components).

## Komponenten in diesem Artikel

- [Log Viewer](https://wingo-ui.com/components/log-viewer) (Pro): A React log viewer for streaming logs up to 100,000 lines: level colors, level and regex filters, tail follow, ANSI colors and JSON details. Installation: `npx wingo-ui@latest add log-viewer`
- [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom) (Pro): A React hook that keeps a chat, a log or streaming output pinned to its bottom while it grows, and lets go when the reader scrolls up. Installation: `npx wingo-ui@latest add use-stick-to-bottom`
- [Virtual List](https://wingo-ui.com/components/virtual-list) (Pro): A React virtual list for long and endless lists at 60fps, with sticky group headers, infinite loading and scroll restoration. Installation: `npx wingo-ui@latest add virtual-list`

## Häufige Fragen

### Wie zeige ich Logs live in React an?

Öffne in einem Effect eine EventSource oder einen WebSocket, häng jede Nachricht an einen Puffer außerhalb des React-State und render mit einem Virtualizer nur die sichtbaren Zeilen. Mit dem Log Viewer von Wingo UI rufst du im Effect api.append(event.data) auf, und er bündelt alles, was in einem Frame ankommt, zu einem einzigen Render.

### Wie stoppe ich Auto-Scroll, wenn jemand hochscrollt?

Werte jedes Scrollen nach oben als Absicht der lesenden Person und schalte das Folgen sofort ab, schon beim wheel-Event, bevor sich scrollTop geändert hat. Schalte es erst wieder ein, wenn die Person ganz nach unten zurückscrollt oder einen Button zur neuesten Zeile drückt. Der Log Viewer von Wingo UI folgt wieder, sobald man bis auf 4px ans Ende scrollt; der Hook useStickToBottom nutzt einen Schwellwert von 72px, den du ändern kannst.

### Wie viele Log-Zeilen schafft eine React-Komponente?

Mit Virtualisierung setzt der Speicher die Grenze, nicht das DOM, denn gerendert werden nur die Zeilen auf dem Bildschirm plus ein kleiner Overscan. Der Log Viewer hält standardmäßig bis zu 100.000 Zeilen (Prop maxLines) und verwirft darüber die ältesten, mit einem Footer, der sagt, wie viele weggefallen sind.

### Wie filtere ich Logs in React mit einem regulären Ausdruck?

Kompilier das Muster einmal mit new RegExp(text, "i") in einem try / catch, markier das Feld als ungültig, wenn es wirft, und prüf jede Zeile nach einem kurzen Debounce gegen den kompilierten Ausdruck. Verzichte bei test() auf das Flag g, denn eine globale Regex merkt sich lastIndex zwischen den Aufrufen und überspringt Treffer.

### Ist der Log Viewer von Wingo UI kostenlos?

Nein. Der Log Viewer, der Hook useStickToBottom und die Virtual List gehören zu Wingo UI Pro. Du installierst sie als Quellcode mit npx wingo-ui@latest add log-viewer.

---

Quelle: https://wingo-ui.com/de/blog/react-log-viewer
