JSON Toolkit

FAQ zu Performance und Sicherheit: langsam, ruckelig und nicht vertrauenswürdige Daten

Fazit: Warum eine JSON-Datei mit zig MB sich nicht öffnet, wie teuer Tiefenklonen ist, wie man __proto__-Verschmutzung verhindert und wo XSS eindringt – zehn Erste-Hilfe-Schlussfolgerungen nach Symptom geordnet.

Fortgeschritten~13 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für die FAQ zu Performance und Sicherheit

Kurz gesagt: Diagnostizieren Sie zuerst nach Symptom – Parsen, Rendern oder Speicher – und arbeiten Sie dann die Behebungsreihenfolge durch: Datenvolumen reduzieren, Tiefenklonen stoppen, Parsen in einen Worker mit virtuellem Scrollen verlagern und alles über 50 MB streamen. Bei nicht vertrauenswürdiger Eingabe begrenzen Sie Größe und Verschachtelungstiefe, bevor Sie __proto__, constructor und prototype filtern.

„JSON ist langsam“ ist neun von zehn Malen ungenau; langsam ist meist das Rendern, das Tiefenklonen oder die falsche Reihenfolge der Operationen.

1. Warum es langsam ist

Das Parsen einer großen JSON-Datei ist langsam. Wie optimiere ich es?

Lokalisieren Sie zuerst den Engpass, sprechen Sie dann über Optimierung. Die Stufen unterscheiden sich um Größenordnungen:

Stufe Ungefähre Zeit für 10 MB Daten Warum sie der Engpass ist
Netzwerkübertragung Hängt von der Bandbreite ab Unkomprimiertes JSON komprimiert meist auf etwa 1/5
JSON.parse 20–80 ms Eine native Implementierung, bereits schnell
Rekursive Traversierung / Tiefenklon 200–2000 ms Reine JS-Knoten-für-Knoten-Verarbeitung
DOM-Rendering (Baumansicht) Sekunden, friert leicht ein Die Knotenzahl explodiert

Die Optimierungsreihenfolge sollte also sein:

  1. Datenvolumen reduzieren: Die API paginieren lassen, nur benötigte Felder zurückgeben und gzip/brotli aktivieren;
  2. Tiefenklonen vermeiden: siehe nächster Abschnitt; das ist der häufigste versteckte Kostenfaktor;
  3. Lazy Rendering: in der Baumansicht nur die aufgeklappten Ebenen rendern, statt Zehntausende DOM-Knoten auf einmal zu erzeugen;
  4. Streaming: nicht alles auf einmal parsen, wenn die Daten 10 MB übersteigen.

Ist der Engpass wirklich JSON.parse, bedeutet das, die Daten sind bereits zig MB oder mehr, und der richtige Schritt ist ein Streaming-Parser:

import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';

// Handle array elements one by one; memory usage is independent of the total size
const pipeline = fs.createReadStream('big.json')
  .pipe(parser())
  .pipe(streamArray());

pipeline.on('data', ({ value }) => {
  processItem(value);
});

Ein JSON mit zig MB stürzt den Browser beim Öffnen ab. Was nun?

Bestätigen Sie zuerst, ob es „abstürzt“ oder „einfriert“. Die beiden brauchen völlig unterschiedliche Behandlung:

  • Einfrieren: Der Tab zeigt „Seite reagiert nicht“ und erholt sich womöglich nach zig Sekunden. Die Ursache sind lange Aufgaben, die den Hauptthread sättigen;
  • Absturz: Der Tab wird zu „Aw, Snap“, meist Speichererschöpfung (OOM).

So oder so, verarbeiten Sie es nicht auf dem Hauptthread. Verlagern Sie das Parsen in einen Web Worker:

// worker.js
self.onmessage = (event) => {
  const parsed = JSON.parse(event.data);
  self.postMessage(parsed);
};

Ein Worker löst „die UI friert nicht ein“, nicht die Speicherobergrenze. Ein Browser-Tab hat typischerweise 1–4 GB verfügbar; ein 200-MB-JSON, in Objekte geparst, kann 1 GB oder mehr belegen (jedes Schlüssel-Wert-Paar trägt Objekt-Overhead), und mit Rendering obendrauf erreicht es leicht die Grenze.

Die echte Lösung ist, es nie ganz zu laden:

Datengröße Ansatz
< 5 MB Direkt mit JSON.parse parsen, Rendering beobachten
5–50 MB Worker-Parsen + virtuelles Scrollen
> 50 MB Streaming-Parsen, nur die benötigten Felder behalten
> 500 MB Das Format ändern (Parquet / SQLite / geshardetes JSON Lines)

Ist JSON.parse(JSON.stringify(obj)) ein Tiefenklon?

Es ist ein Tiefenklon, aber es „verändert Daten stillschweigend“ an mehreren Stellen, ist also kein geeignetes allgemeines Klonwerkzeug.

const source = {
  date: new Date(),
  fn: () => {},
  undef: undefined,
  nan: NaN,
  map: new Map(),
  nested: { a: 1 },
};
JSON.parse(JSON.stringify(source));
// {
//   date: "2026-10-30T03:00:00.000Z",  // Date becomes a string
//   nan: null,                          // NaN becomes null
//   nested: { a: 1 }                    // only this level is correct
// }
// fn and undef disappear, map becomes {}

Die Liste der Probleme:

Typ Ergebnis
Date Wird zu einem ISO-String
NaN / Infinity Wird zu null
undefined / Funktionen / Symbol Verschwindet in Objekten, wird in Arrays zu null
Map / Set Wird zu {}
BigInt Wirft TypeError
Zirkuläre Referenzen Wirft TypeError

Moderne Browser haben structuredClone, das Date, Map, Set und zirkuläre Referenzen unterstützt, aber keine Funktionen:

const copy = structuredClone(source);

Handelt es sich nur um einen flachen Konfigurations-Merge, ist der Spread-Operator die günstigste Option: { ...config, timeout: 3000 }.

Warum ist das Aufklappen der Baumansicht so ruckelig?

Weil jeder Knoten ein DOM-Element ist, und sobald die Knotenzahl Zehntausende erreicht, explodieren die Layoutkosten des Browsers.

Ein 10-MB-JSON kann 50k–100k Blattknoten enthalten. Selbst wenn Sie nur den sichtbaren Bereich rendern, dauert der einmalige DOM-Aufbau Hunderte Millisekunden bis Sekunden, und jedes nachfolgende Scrollen löst einen Reflow aus.

Der richtige Ansatz:

  1. Virtuelles Scrollen: nur die paar Dutzend Zeilen im Viewport rendern und DOM-Knoten beim Scrollen wiederverwenden;
  2. Standardmäßig zuklappen: nur die ersten zwei Ebenen aufklappen und tiefere Ebenen bei Bedarf rendern;
  3. Renderumfang begrenzen: über 10k Knoten „Daten zu groß, nutzen Sie die Suche zur Lokalisierung“ anzeigen, statt ein Rendern zu erzwingen.

Das ist auch der Grund, warum der Editor auf dieser Website bei großen Dateien anmutig degradiert – nicht weil er sie technisch nicht rendern kann, sondern weil das Rendern unbrauchbar wäre.

2. Warum es angegriffen wird

Wie funktioniert __proto__-Injection?

Prototype Pollution. Ein Angreifer konstruiert JSON mit einem __proto__-Schlüssel; mergt Ihr Code es ohne Filterung in ein Objekt, überschreibt es Object.prototype und beeinflusst jedes Objekt in der Laufzeit.

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');

// Dangerous code: recursive merge without checking key names
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, payload);
({}).isAdmin;  // true — every object has been polluted

Der zentrale Fakt: JSON.parse selbst verunreinigt den Prototyp nicht; {"__proto__": ...} parst zu einer gewöhnlichen eigenen Eigenschaft. Gefährlich sind Mergen, Zuweisung, Object.assign, lodash.merge und ähnliche Operationen nach dem Parsen.

Drei Verteidigungen:

  1. Schlüsselnamen filtern: __proto__, constructor und prototype beim Mergen überspringen;
  2. Object.create(null) als Container verwenden, damit es keine Prototypenkette zu verschmutzen gibt;
  3. Eine Map verwenden, um nicht vertrauenswürdige Schlüssel-Wert-Paare zu speichern.
function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    const value = source[key];
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      target[key] = safeMerge(target[key] ?? {}, value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

Beachten Sie außerdem, dass for...in geerbte Eigenschaften durchläuft; Object.keys() ist sicherer.

Worauf sollte ich beim Parsen nicht vertrauenswürdigen JSONs achten?

In dieser Reihenfolge – geringe Kosten, hoher Nutzen:

  1. Größe begrenzen: alles über der Schwelle sofort ablehnen, ohne es zuerst in den Speicher zu lesen;
  2. Tiefe begrenzen: ein JSON, das Hunderte Ebenen tief verschachtelt ist, kann den Stack eines rekursiven Parsers überlaufen lassen;
  3. Schlüsselzahl begrenzen: verhindert Hash-Kollisionsangriffe und Speicherverstärkung;
  4. Gefährliche Schlüssel filtern: __proto__, constructor, prototype;
  5. Die Struktur validieren: durch ein Schema laufen lassen, um Typen und Pflichtfelder zu bestätigen;
  6. Content-Type nicht vertrauen: der deklarierte Typ und der tatsächliche Inhalt können auseinandergehen.

Eine Tiefenprüfung ist billig; Sie können vor dem Parsen einen groben Regex-Filter verwenden oder nach dem Parsen traversieren:

function measureDepth(value, depth = 0) {
  if (value === null || typeof value !== 'object') {
    return depth;
  }
  let maxDepth = depth;
  for (const key of Object.keys(value)) {
    maxDepth = Math.max(maxDepth, measureDepth(value[key], depth + 1));
  }
  return maxDepth;
}

Aber dieser Code ist selbst rekursiv und kann bei böswillig tiefen Daten trotzdem den Stack überlaufen lassen. Der sichere Ansatz ist, das Limit während des Parsens zu erzwingen, mit einem Parser, der eine Tiefenoption unterstützt (manche Bibliotheken bieten einen maxDepth-Parameter).

Wie entsteht XSS durch JSON?

JSON selbst ist kein HTML und führt sich nicht aus. XSS entsteht an der Stelle, an der „ein JSON-Wert in HTML verkettet wird“.

Drei häufige Pfade:

Pfad Beispiel Verteidigung
Serverseitige Template-Verkettung res.send('<div>' + data.name + '</div>') HTML-Escaping bei der Ausgabe
Frontend innerHTML el.innerHTML = data.html textContent verwenden oder bereinigen
Inline-Script-Einbettung <script>var d = ${JSON.stringify(data)}</script> <, >, & escapen

Der dritte ist der subtilste. JSON.stringify escaped < nicht, Daten mit </script><script>alert(1)</script> können das Script-Tag also vorzeitig schließen:

const data = { name: '</script><script>alert(1)</script>' };
// Output: <script>var d = {"name":"</script><script>alert(1)</script>"}</script>

Die Behebung ist, gefährliche Zeichen in Unicode-Escapes umzuwandeln:

function safeJsonForHtml(data) {
  return JSON.stringify(data)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}

Sollte ich die Verschachtelungstiefe begrenzen?

Ja. Das ist eine der Verteidigungen mit dem besten Kosten-Nutzen-Verhältnis.

Die Folgen ohne Begrenzung: Rekursive Funktionen überlaufen den Stack, JSON.parse stürzt in manchen Implementierungen bei zu großer Tiefe ab, und die Serialisierung sprengt den Stack auf dieselbe Weise. Ein 1 KB [[[[...]]]] genügt, um einen ungeschützten Parser zu Fall zu bringen.

Eine vernünftige Schwelle hängt vom Geschäft ab:

Datentyp Vorgeschlagenes Limit
Gewöhnliche Geschäftsobjekte 10–20 Ebenen
Baumstrukturen (Org-Charts, Verzeichnisse) 50–100 Ebenen
Allgemeines API-Gateway 20 Ebenen

Was ist JSON-Hijacking?

Eine historische Schwachstelle: Frühe Browser erlaubten cross-origin <script src>, JSON zu laden und auszuführen, und Angreifer stahlen Daten, indem sie den Array-Konstruktor oder Object.prototype.__defineSetter__ neu definierten.

Moderne Schutzmaßnahmen sind ziemlich vollständig:

  • Browser erlauben nicht mehr, Array-Literale mit __defineSetter__ zu entführen;
  • Ein korrekter Content-Type: application/json verhindert die Skriptausführung;
  • X-Content-Type-Options: nosniff verhindert MIME-Sniffing.

Ältere Systeme tragen noch Restrisiko. Praktischer Rat: ein Objekt auf oberster Ebene verwenden, kein nacktes Array. Eine Hülle wie {"data": [...]} hält die Antwort davon ab, eine gültige JavaScript-Anweisung zu sein ({ wird als Codeblock behandelt), und blockiert Hijacking auf Syntaxebene.

Welche Risiken birgt das Loggen von JSON?

Drei:

  1. Sensible Informationen auf Platte geschrieben: Telefonnummern, Ausweisnummern, Tokens und Klartextpasswörter landen im Logging-System, das meist lockerere Berechtigungen und längere Aufbewahrung hat;
  2. Log-Injection: Ein JSON-Wert mit Zeilenumbrüchen und gefälschten Log-Präfixen spaltet einen Datensatz in mehrere und verunreinigt den Audit-Trail;
  3. Größenexplosion: Eine ganze 10-MB-Antwort ins Log zu schreiben füllt das Logging-System schnell.

Ansatz:

// Redact and truncate before logging
function safeLog(data) {
  const cloned = structuredClone(data);
  for (const key of ['password', 'token', 'idCard', 'phone']) {
    if (key in cloned) cloned[key] = '***';
  }
  const text = JSON.stringify(cloned);
  return text.length > 2000 ? text.slice(0, 2000) + '...(truncated)' : text;
}

Strukturierte Logging-Systeme (etwa pino) escapen Zeilenumbrüche selbst, aber Redigieren und Kürzen bleibt Ihre Aufgabe.

3. Kurzreferenz

Symptom Zuerst prüfen Behebung
Langsames Parsen Datenvolumen, ob Tiefenklonen beteiligt ist Chunking/Streaming, JSON.parse(JSON.stringify()) entfernen
Seite friert ein Lange Aufgaben auf dem Hauptthread, Zahl gerenderter Knoten Worker + virtuelles Scrollen
Tab stürzt ab Speicherverbrauch Nicht alles laden; auf Streaming umstellen
Tiefenklon verliert Daten Date, NaN, Map structuredClone verwenden
Objekte haben plötzlich zusätzliche Eigenschaften Prototype Pollution __proto__ filtern, eine Map verwenden
Ein Script poppt auf Strings in HTML verkettet < escapen, textContent verwenden
Stack-Overflow Verschachtelungstiefe Auf 10–20 Ebenen begrenzen
Logs laufen voll Die ganze Antwort wird geloggt Kürzen + redigieren