JSON Toolkit

FAQ dotyczące wydajności i bezpieczeństwa: wolne, zacinające się i niezaufane dane

Podsumowanie: Dlaczego plik JSON o rozmiarze kilkudziesięciu MB się nie otwiera, ile kosztuje głębokie klonowanie, jak zapobiec zanieczyszczeniu __proto__ i gdzie wchodzi XSS — dziesięć wniosków pierwszej pomocy uporządkowanych według objawów.

Średniozaawansowany~13 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka FAQ dotyczącego wydajności i bezpieczeństwa

Najważniejsze: najpierw diagnozuj według objawu — parsowanie, renderowanie czy pamięć — a potem przejdź przez kolejność napraw: ogranicz objętość danych, przestań głęboko klonować, przenieś parsowanie do Workera z wirtualnym przewijaniem i strumieniuj wszystko powyżej 50 MB. Dla niezaufanych danych wejściowych ogranicz rozmiar i głębokość zagnieżdżenia, zanim przefiltrujesz __proto__, constructor i prototype.

„JSON jest wolny” jest nieścisłe w dziewięciu przypadkach na dziesięć; naprawdę wolne jest zwykle renderowanie, głębokie klonowanie albo zła kolejność operacji.

1. Dlaczego jest wolno

Parsowanie dużego pliku JSON jest wolne. Jak to zoptymalizować?

Najpierw zlokalizuj wąskie gardło, o optymalizacji rozmawiaj później. Etapy różnią się o kilka rzędów wielkości:

Etap Przybliżony czas dla 10 MB danych Dlaczego jest wąskim gardłem
Transfer sieciowy Zależy od przepustowości Nieskompresowany JSON zwykle kompresuje się do około 1/5
JSON.parse 20–80 ms Implementacja natywna, już szybka
Przejście rekurencyjne / głęboki klon 200–2000 ms Przetwarzanie węzeł po węźle w czystym JS
Renderowanie DOM (widok drzewa) Sekundy, łatwo się zawiesza Liczba węzłów eksploduje

Kolejność optymalizacji powinna więc być taka:

  1. Zmniejsz objętość danych: pozwól API paginować, zwracać tylko potrzebne pola i włącz gzip/brotli;
  2. Unikaj głębokiego klonowania: zobacz następną sekcję; to najczęstszy ukryty koszt;
  3. Leniwe renderowanie: w widoku drzewa renderuj tylko rozwinięte poziomy, zamiast generować dziesiątki tysięcy węzłów DOM naraz;
  4. Strumieniowanie: nie parsuj wszystkiego naraz, gdy dane przekraczają 10 MB.

Jeśli wąskim gardłem naprawdę jest JSON.parse, to dane mają już dziesiątki MB lub więcej, a właściwym ruchem jest parser strumieniowy:

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);
});

JSON o rozmiarze kilkudziesięciu MB zawiesza przeglądarkę zaraz po otwarciu. Co teraz?

Najpierw potwierdź, czy „zawiesza się”, czy „crashuje”. Dwa przypadki wymagają zupełnie innego podejścia:

  • Zawieszenie: karta pokazuje „Strona nie odpowiada” i może wrócić po dziesiątkach sekund. Przyczyną są długie zadania wysycające wątek główny;
  • Crash: karta zmienia się w „Aw, Snap”, zwykle wyczerpanie pamięci (OOM).

W obu przypadkach nie przetwarzaj tego w wątku głównym. Przenieś parsowanie do Web Workera:

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

Worker rozwiązuje „UI się nie zawiesza”, a nie sufit pamięci. Karta przeglądarki ma zwykle 1–4 GB dostępne; JSON 200 MB sparsowany do obiektów może zająć 1 GB lub więcej (każda para klucz-wartość niesie narzut obiektu), a z renderowaniem na wierzchu łatwo trafia w limit.

Prawdziwym rozwiązaniem jest nigdy nie wczytywać całości:

Rozmiar danych Podejście
< 5 MB Parsuj bezpośrednio JSON.parse, obserwuj renderowanie
5–50 MB Parsowanie w Workerze + wirtualne przewijanie
> 50 MB Parsowanie strumieniowe, zachowaj tylko potrzebne pola
> 500 MB Zmień format (Parquet / SQLite / podzielony JSON Lines)

Czy JSON.parse(JSON.stringify(obj)) to głęboki klon?

To głęboki klon, ale „po cichu zmienia dane” w kilku miejscach, więc nie nadaje się jako ogólne narzędzie do klonowania.

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 {}

Lista problemów:

Typ Wynik
Date Staje się łańcuchem ISO
NaN / Infinity Staje się null
undefined / funkcje / Symbol Znika wewnątrz obiektów, staje się null wewnątrz tablic
Map / Set Staje się {}
BigInt Rzuca TypeError
Odwołania cykliczne Rzuca TypeError

Współczesne przeglądarki mają structuredClone, który wspiera Date, Map, Set i odwołania cykliczne, ale nie funkcje:

const copy = structuredClone(source);

Jeśli to tylko płytkie scalanie konfiguracji, operator spread jest najtańszą opcją: { ...config, timeout: 3000 }.

Dlaczego rozwijanie widoku drzewa tak się zacina?

Bo każdy węzeł to element DOM, a gdy liczba węzłów sięga dziesiątek tysięcy, koszt układu przeglądarki eksploduje.

JSON 10 MB może zawierać 50k–100k węzłów liści. Nawet jeśli renderujesz tylko widoczny obszar, jednorazowe zbudowanie DOM zajmuje setki milisekund do sekund, a każde późniejsze przewinięcie wyzwala reflow.

Właściwe podejście:

  1. Wirtualne przewijanie: renderuj tylko kilkadziesiąt wierszy w widocznym obszarze i używaj ponownie węzłów DOM podczas przewijania;
  2. Zwijanie domyślnie: rozwiń tylko dwa pierwsze poziomy i renderuj głębsze poziomy na żądanie;
  3. Ogranicz skalę renderowania: powyżej 10k węzłów pokaż „Dane zbyt duże, użyj wyszukiwania, aby je zlokalizować”, zamiast wymuszać renderowanie.

To też dlatego edytor w tej witrynie degraduje się łagodnie dla dużych plików — nie bo technicznie nie potrafi ich wyrenderować, ale bo renderowanie ich byłoby nieużywalne.

2. Dlaczego jest atakowane

Jak działa wstrzyknięcie __proto__?

Zanieczyszczenie prototypu. Atakujący konstruuje JSON z kluczem __proto__; jeśli twój kod scala go do obiektu bez filtrowania, nadpisuje Object.prototype i wpływa na każdy obiekt w środowisku wykonania.

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

Kluczowy fakt: sam JSON.parse nie zanieczyszcza prototypu; {"__proto__": ...} parsuje się do zwykłej właściwości własnej. Niebezpieczne są scalanie, przypisanie, Object.assign, lodash.merge i podobne operacje po parsowaniu.

Trzy obrony:

  1. Filtruj nazwy kluczy: pomijaj __proto__, constructor i prototype podczas scalania;
  2. Użyj Object.create(null) jako kontenera, aby nie było łańcucha prototypów do zanieczyszczenia;
  3. Użyj Map do przechowywania niezaufanych par klucz-wartość.
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;
}

Zauważ też, że for...in przechodzi właściwości dziedziczone; Object.keys() jest bezpieczniejsze.

Na co uważać przy parsowaniu niezaufanego JSON?

Rób to w tej kolejności — niski koszt, wysoki zysk:

  1. Ogranicz rozmiar: odrzucaj wszystko ponad próg natychmiast, bez wczytywania do pamięci;
  2. Ogranicz głębokość: JSON zagnieżdżony setki poziomów w głąb może przepełnić stos parsera rekurencyjnego;
  3. Ogranicz liczbę kluczy: zapobiega atakom kolizji haszowania i wzmocnieniu pamięci;
  4. Filtruj niebezpieczne klucze: __proto__, constructor, prototype;
  5. Waliduj strukturę: przepuść przez Schema, aby potwierdzić typy i wymagane pola;
  6. Nie ufaj Content-Type: deklarowany typ i rzeczywista treść mogą się różnić.

Kontrola głębokości jest tania; możesz użyć zgrubnego filtra regex przed parsowaniem albo przejścia po parsowaniu:

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;
}

Ale ten kod sam jest rekurencyjny i nadal może przepełnić stos na złośliwie głębokich danych. Bezpiecznym podejściem jest wymuszenie limitu podczas parsowania, przez parser wspierający opcję głębokości (niektóre biblioteki udostępniają parametr maxDepth).

Jak dochodzi do XSS przez JSON?

Sam JSON nie jest HTML-em i nie wykonuje się. XSS powstaje w momencie, gdy „wartość JSON jest wklejona do HTML”.

Trzy częste ścieżki:

Ścieżka Przykład Obrona
Konkatenacja szablonu po stronie serwera res.send('<div>' + data.name + '</div>') HTML-escape przy wyjściu
Frontendowy innerHTML el.innerHTML = data.html Użyj textContent albo sanityzuj
Osadzenie w skrypcie inline <script>var d = ${JSON.stringify(data)}</script> Escape'uj <, >, &

Trzecia jest najbardziej subtelna. JSON.stringify nie escape'uje <, więc dane zawierające </script><script>alert(1)</script> mogą zamknąć tag skryptu wcześniej:

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

Poprawką jest zamiana niebezpiecznych znaków na ucieczki Unicode:

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

Czy ograniczać głębokość zagnieżdżenia?

Tak. To jedna z obron o najlepszym stosunku kosztu do korzyści.

Konsekwencje braku limitu: funkcje rekurencyjne przepełniają stos, JSON.parse crashuje w niektórych implementacjach przy zbyt dużej głębokości, a serializacja wysadza stos w ten sam sposób. [[[[...]]]] o rozmiarze 1 KB wystarczy, by powalić niezabezpieczony parser.

Rozsądny próg zależy od biznesu:

Typ danych Sugerowany limit
Zwykłe obiekty biznesowe 10–20 poziomów
Struktury drzewa (organigramy, katalogi) 50–100 poziomów
Ogólna brama API 20 poziomów

Czym jest JSON hijacking?

Historyczna podatność: wczesne przeglądarki pozwalały międzyźródłowemu <script src> ładować i wykonywać JSON, a atakujący kradli dane, przedefiniowując konstruktor Array albo Object.prototype.__defineSetter__.

Współczesne zabezpieczenia są dość kompletne:

  • Przeglądarki nie pozwalają już na porywanie literałów tablic przez __defineSetter__;
  • Poprawny Content-Type: application/json zapobiega wykonaniu skryptu;
  • X-Content-Type-Options: nosniff zapobiega sniffingowi MIME.

Starsze systemy nadal niosą ryzyko resztkowe. Praktyczna rada: używaj obiektu na wierzchołku, a nie nagiej tablicy. Opakowanie takie jak {"data": [...]} nie pozwala odpowiedzi być poprawnym wyrażeniem JavaScript ({ jest traktowane jako blok kodu), blokując porywanie na poziomie składni.

Jakie ryzyka niesie logowanie JSON?

Trzy:

  1. Wrażliwe informacje zapisywane na dysk: numery telefonów, numery dowodów, tokeny i hasła w postaci jawnej trafiają do systemu logów, który zwykle ma luźniejsze uprawnienia i dłuższą retencję;
  2. Wstrzyknięcie do logów: wartość JSON zawierająca nowe linie i sfałszowane prefiksy logów dzieli jeden rekord na kilka i zanieczyszcza ścieżkę audytu;
  3. Eksplozja rozmiaru: zapisanie całej odpowiedzi 10 MB do logu szybko zapełnia system logów.

Podejście:

// 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;
}

Strukturalne systemy logów (takie jak pino) same escape'ują nowe linie, ale redakcja i obcinanie nadal należą do ciebie.

3. Szybka ściągawka

Objaw Sprawdź najpierw Poprawka
Wolne parsowanie Objętość danych, czy w grę wchodzi głębokie klonowanie Dzielenie/strumieniowanie, usuń JSON.parse(JSON.stringify())
Strona się zawiesza Długie zadania w wątku głównym, liczba renderowanych węzłów Worker + wirtualne przewijanie
Karta crashuje Zużycie pamięci Nie wczytuj wszystkiego; przejdź na strumieniowanie
Głęboki klon gubi dane Date, NaN, Map Użyj structuredClone
Obiekty nagle mają dodatkowe właściwości Zanieczyszczenie prototypu Filtruj __proto__, użyj Map
Wyskakuje skrypt Łańcuchy sklejone do HTML Escape'uj <, użyj textContent
Przepełnienie stosu Głębokość zagnieżdżenia Limit 10–20 poziomów
Logi się zapełniają Cała odpowiedź jest logowana Obcinaj + redaguj