JSON Toolkit

Große JSON-Dateien parsen, rendern und streamen

Fazit: Parsen ist oft nicht der Engpass; Rendern und Kopieren sind es. Optimieren Sie entlang der vier Schichten „parsen → durchlaufen → rendern → transportieren“, damit Ihr Aufwand nicht verpufft.

Fortgeschritten~13 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-31
Titelbild für Große JSON-Dateien parsen, rendern und streamen

Kurz gesagt: Messen Sie zuerst Parsen, Durchlaufen, Rendern und Transport getrennt, und arbeiten Sie dann in dieser Reihenfolge – Kopien reduzieren, mit virtuellem Scrollen bei Bedarf aufklappen, Arbeit in einen Worker verlagern, auf Streaming umstellen und erst dann das Format ändern. Streaming lohnt sich nur, wenn die Datei nicht in den Speicher passt oder Sie nur eine kleine Teilmenge ihrer Felder brauchen.

Die Schlussfolgerung „JSON.parse ist langsam“ ist acht von zehn Malen falsch. Langsam ist, was Sie nach dem Parsen tun.

1. Erst messen, dann optimieren

Bei derselben 10-MB-JSON-Datei unterscheidet sich die Zeit in verschiedenen Stufen um Größenordnungen:

Stufe Typische Dauer Hauptkosten
JSON.parse (nativ) Zehn bis knapp über hundert Millisekunden Ein einzelner linearer Scan plus Objektkonstruktion
Tiefenkopie (structuredClone / rekursiv) Womöglich das 1- bis 3-fache des Parsens Neuaufbau des ganzen Objektgraphen
Tiefenvergleich / Diff Womöglich noch einmal das Doppelte Rekursive Traversierung plus Stringvergleich
Rendern in einen DOM-Baum Friert bei Zehntausenden Knoten ein Layout und Reflow
Stringverkettung, dann Serialisierung Proportional zur Ausgabegröße Verdoppelt den Speicher

Messen Sie also, bevor Sie handeln: Im Browser jede Stufe mit performance.now() umwickeln oder console.time verwenden; in Node process.memoryUsage() nutzen, um das Heap-Wachstum zu beobachten. Keine Baseline, keine Optimierung.

2. Parsing-Schicht: drei Strategien

2.1 Vollständiges Parsen (die Standardwahl)

const data = JSON.parse(text);

Die native Implementierung erledigt Scannen und Objektkonstruktion in C++, eine reine JS-Bibliothek kann sie kaum schlagen. Verwenden Sie sie, solange die folgenden drei Bedingungen gelten:

  • Der Text passt vollständig in den Speicher (meist < 100 MB);
  • Die meisten Felder werden nach dem Parsen tatsächlich gebraucht;
  • Der Grad, in dem die Daten nicht vertrauenswürdig sind, ist handhabbar.

2.2 Inkrementelles Streaming-Parsen

Mit einem SAX-artigen Parser wie stream-json können Sie die Ziel-Felder heraussensen, ohne einen vollständigen Objektbaum zu bauen:

import { createReadStream } from 'node:fs';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import { streamArray } from 'stream-json/streamers/StreamArray';

const pipeline = createReadStream('huge.json')
  .pipe(parser())
  .pipe(pick({ filter: 'items' }))
  .pipe(streamArray());

for await (const { value } of pipeline) {
  // Process one element at a time; memory is independent of element count
  handleItem(value);
}

Kosten:

  • Die Codekomplexität steigt deutlich und die Fehlerbehandlung wird unübersichtlicher;
  • Nur sequenzieller Zugriff ist möglich, kein wahlfreier Zugriff;
  • Es ist unfreundlich gegenüber Anforderungen, die globale Aggregation brauchen.

Entscheidungsregel: Streaming lohnt sich nur, wenn die Datei zu groß zum Halten ist oder Sie nur eine sehr kleine Teilmenge ihrer Felder brauchen.

2.3 Das Format ändern

Sind die Daten „ein Stapel Datensätze“ statt „ein einzelnes Dokument“, hätten Sie von Anfang an JSON Lines verwenden sollen. Ein zeilengetrenntes Format verwandelt „Streaming“ in „zeilenweises Lesen“ und senkt die Implementierungskomplexität um eine Größenordnung.

3. Traversierungsschicht: eine Kopie weniger gewinnt die halbe Schlacht

// Anti-pattern: deep copying first "for safety"
const safeCopy = structuredClone(bigData);   // Memory doubles immediately

// Good pattern: pass references directly in read-only scenarios
function renderSummary(data: OrderDocument): Summary {
  return { total: data.items.length, amount: data.total };
}

Häufige versteckte Kopien:

  • JSON.parse(JSON.stringify(x)) als „Tiefenkopie“ verwendet – die teuerste Art;
  • { ...data } kopiert nur eine Ebene, aber bei einem großen Objekt kopiert es trotzdem das Referenzarray aller Top-Level-Schlüssel;
  • Array.prototype.map erzeugt ein neues Array, was bei vielen Elementen ebenfalls kostet;
  • Reaktivitäts-Proxies von Frameworks (Vues reactive) proxyen große Objekte rekursiv, und der Overhead kann höher sein als das Parsen selbst.

Praktischer Rat: Behalten Sie die „Rohdaten“ als einfaches unveränderliches Objekt und wickeln Sie es nicht in Reaktivität; berechnen und cachen Sie die abgeleiteten Daten, die Sie separat anzeigen müssen.

4. Rendering-Schicht: nicht den ganzen Baum ins DOM stopfen

Eine 5-MB-JSON-Datei hat grob 30.000 bis 100.000 Knoten; sie alle als DOM-Elemente zu rendern lässt den Browser nicht mehr reagieren.

Drei praktikable Ansätze:

4.1 Bei Bedarf aufklappen

Rendern Sie nur die direkten Kinder des Wurzelknotens und berechnen Sie die nächste Ebene erst beim Aufklappen. Das ist der Standardansatz für Baum-Viewer:

type TreeNode = { key: string; value: unknown; expanded: boolean };

/**
 * Returns only the currently visible nodes, avoiding building the whole tree at once.
 * @param rootNode Root node
 * @param expandedPaths Set of paths that have been expanded
 * @returns List of visible nodes
 */
function collectVisibleNodes(rootNode: TreeNode, expandedPaths: Set<string>): TreeNode[] {
  const visibleNodes: TreeNode[] = [];

  /**
   * Recursively collects the child nodes of expanded branches.
   * @param currentNode Current node
   * @param currentPath Current path
   */
  const walk = (currentNode: TreeNode, currentPath: string): void => {
    visibleNodes.push(currentNode);
    if (!expandedPaths.has(currentPath) || typeof currentNode.value !== 'object' || currentNode.value === null) {
      return;
    }
    for (const [childKey, childValue] of Object.entries(currentNode.value)) {
      walk({ key: childKey, value: childValue, expanded: false }, `${currentPath}/${childKey}`);
    }
  };

  walk(rootNode, '');
  return visibleNodes;
}

4.2 Virtuelles Scrollen

Sobald aufgeklappte Knoten ein paar hundert übersteigen, rendern Sie nur die paar Dutzend im Viewport und reservieren den Rest mit Platzhalterhöhen. Am wirksamsten ist es in listenartigen Ansichten (Tabellen, Logs).

4.3 Nur Zusammenfassungen zeigen

Zeigen Sie zuerst „Struktur + Typ + Anzahl“ (etwa items: Array(12000)); erst wenn der Nutzer einen Eintrag öffnet, holen Sie den konkreten Wert. Dieser Trick funktioniert am besten für das Szenario „ich will nur bestätigen, dass die Felder stimmen“.

5. Hauptthread: Arbeit in einen Worker verlagern, wenn nötig

Parsen, Formatieren und Diffen von allem über 1 MB sollten in einen Web Worker:

// json.worker.ts
self.onmessage = (event: MessageEvent<{ text: string }>) => {
  try {
    const parsed = JSON.parse(event.data.text);
    self.postMessage({ ok: true, data: parsed });
  } catch (parseError) {
    self.postMessage({ ok: false, message: (parseError as Error).message });
  }
};
/**
 * Parses JSON inside a Worker so the main thread is not blocked.
 * @param text JSON text
 * @returns Parse result
 */
function parseInWorker(text: string): Promise<unknown> {
  return new Promise((resolvePromise, rejectPromise) => {
    const worker = new Worker(new URL('./json.worker.ts', import.meta.url), { type: 'module' });
    worker.onmessage = (event) => {
      worker.terminate();
      if (event.data.ok) {
        resolvePromise(event.data.data);
      } else {
        rejectPromise(new Error(event.data.message));
      }
    };
    worker.postMessage({ text });
  });
}

Beachten Sie die Kommunikationskosten von Workern: postMessage verwendet standardmäßig strukturiertes Klonen, und ein 10-MB-Objekt einmal zu klonen dauert ebenfalls Zehntausende Millisekunden. Brauchen Sie nur, „es zu parsen und an den Hauptthread zu geben“, erledigen Sie die schwere Arbeit im Worker (parsen + transformieren + validieren) und senden Sie eine Ergebniszusammenfassung zurück.

6. Transport-Schicht: Größenoptimierung

Technik Typischer Gewinn Kosten
gzip / brotli Komprimierte Ausgabe ist oft 10 %–25 % der Originalgröße Erfordert Serverunterstützung
Einrückung und Whitespace entfernen 20 %–40 % Reduktion Nicht menschenlesbar
Feldnamen abkürzen Deutliche Gewinne bei tiefer Verschachtelung Geringere Lesbarkeit; erfordert eine Abbildungstabelle
Zahlen statt Enum-Strings Klein Erfordert ein Wörterbuch
Paginierung / Felder auf Abruf Am größten API-Redesign

Für Seiten für Menschen ist die richtige Kombination: Die API sendet kompaktes, komprimiertes JSON, und die UI formatiert es nur zur Anzeige hübsch. Viele Werkzeugseiten senden standardmäßig erneut hübsch formatierten Text ans Frontend und übertragen so 30 % mehr Bytes umsonst.

7. Wie man benchmarkt

/**
 * Measures the median duration of a synchronous routine so a single jittery run does not skew the result.
 * @param runOnce Function under test
 * @param repeatCount Number of repetitions
 * @returns Median duration in milliseconds
 */
function measureMedian(runOnce: () => void, repeatCount = 9): number {
  const durations: number[] = [];
  for (let roundIndex = 0; roundIndex < repeatCount; roundIndex += 1) {
    const startedAt = performance.now();
    runOnce();
    durations.push(performance.now() - startedAt);
  }
  durations.sort((firstValue, secondValue) => firstValue - secondValue);
  return durations[Math.floor(durations.length / 2)];
}

Drei Disziplinregeln:

  1. Ein festes Beispiel verwenden und nie Daten zufällig generieren, sonst sind die Zahlen nicht vergleichbar;
  2. Mindestens 9 Mal laufen lassen und den Median nehmen; der erste Lauf enthält JIT-Aufwärmen und muss verworfen werden;
  3. Die Laufzeitumgebung festhalten (Node-Version, CPU), sonst sind Maschinen-zu-Maschinen-Vergleiche bedeutungslos.

8. Checkliste

  • [ ] Baseline-Zahlen existieren, damit vorher und nachher verglichen werden können;
  • [ ] Parsen von allem über 1 MB läuft in einem Worker;
  • [ ] Baumansichten klappen bei Bedarf auf, und Listen verwenden virtuelles Scrollen;
  • [ ] Keine sinnlosen Tiefenkopien oder Reaktivitäts-Wrapper;
  • [ ] JSON Lines wird verwendet, wenn die Daten „ein Stapel Datensätze“ sind;
  • [ ] Die API sendet kompaktes JSON, und Formatierung geschieht nur zur Anzeige.