Skip to content
jsonforge.app
Back to blog
Deep-dive8 min read

JSON Serialization Performance: Parsing, Streaming, and Large Documents

Parsing and serializing JSON feels so routine that most of us never measure it, until a large export freezes an event loop, a log pipeline falls behind, or a p99 budget shatters. This article covers where the cost goes: DOM versus streaming parsers, memory blowup on big files, NDJSON for record streams, rough numbers across languages, and when binary formats start paying for themselves.

Why parse cost matters

JSON is text, and turning text into objects takes CPU work: tokenizing, validating escapes and number syntax, allocating strings, building hash tables and arrays. On a typical CRUD service the payloads are small and the cost is noise. It stops being noise when documents grow large, traffic runs high, or parsing sits on a latency-sensitive path, and serialization costs roughly as much again.

Start with a ratio: parse time versus total request time. If a handler spends 3 ms of a 10 ms budget deserializing the body, you have a JSON problem, and the rest of this article lays out your options.

DOM-style and streaming parsers

The parsers you use daily (JSON.parse in JavaScript, json.loads in Python, encoding/json in Go) are DOM-style: they consume the whole document and return a complete in-memory tree. Keep that as your default. It's simple, safe, and lets you read any field in any order.

Streaming parsers work at the token level. Pull parsers such as Python's ijson or Go's json.Decoder hand you one element at a time and you drive the loop; push (SAX-style) parsers fire callbacks as tokens pass and you assemble what you care about. Both let you stop early and skip what you don't need. You save memory more than time: the parser still tokenizes every byte, but you never hold the whole tree.

Memory blowup on large documents

A parsed JSON document takes far more memory than the same bytes on the wire. Each string becomes a heap allocation with its own overhead; each object becomes a hash table with buckets to spare. A rule of thumb across runtimes: the in-memory object graph occupies three to ten times the size of the JSON text.

By that math a 100 MB file can take half a gigabyte of resident memory while parsed, and JSON.parse holds the main thread for the whole parse, freezing everything else in a browser or Node. Multiply by concurrent requests and the garbage-collector pressure alone shows up in your metrics.

The fixes, in order of effort: stream the parse and keep only what you need, split the document into records, or stop treating it as one giant JSON value. The next section covers the standard way to do the last two.

NDJSON for logs and event streams

Newline-delimited JSON (one JSON value per line, no outer array) is the standard format for record-shaped data. Each line parses on its own, so you can process a 10 GB export in constant memory, resume from any byte offset, split the file across workers, and append without rewriting anything.

Three events as NDJSON. Each line is a complete JSON object you can parse on its own.
json
{"ts":"2025-07-22T09:14:02Z","level":"info","event":"order.created","order_id":98421,"total":59.9}
{"ts":"2025-07-22T09:14:03Z","level":"warn","event":"payment.retry","order_id":98422,"attempt":2}
{"ts":"2025-07-22T09:14:05Z","level":"error","event":"inventory.short","sku":"SKU-3312","wanted":3,"available":1}

Wrap the same events in one JSON array and you can read nothing until the closing bracket arrives, and the writer can't emit an event until it's ready to terminate the file. That difference explains why NDJSON dominates logging, queues, and bulk exports.

The trade-off: NDJSON isn't one document. It has no root envelope and no cross-line references, and a corrupted line damages only that line. For event streams, you want that failure mode.

Rough numbers: JavaScript, Python, Go

Order-of-magnitude throughput for stock parsers on one modern core: V8's JSON.parse handles roughly 400–800 MB/s and beats every other mainstream runtime's parser. Go's encoding/json sits around 100–200 MB/s, with jsoniter and sonic well beyond. CPython's stdlib json manages 50–100 MB/s, while orjson and msgspec clear a gigabyte per second.

Treat these as orientation, not benchmarks. Real numbers depend on document shape (many small objects cost more per byte than long strings) and the integer-to-float mix. The structural gap is far larger: a 300 MB log stream takes under a second in Node and over a minute in naive Python. Choosing a fast parser over a default one makes that difference; tuning won't.

On the wire, gzip or brotli shrinks JSON five to ten times, so compress. Compression cuts bytes, not parse CPU, though: the receiver still decompresses and tokenizes the whole payload before reading the first field.

When to move to a binary format

MessagePack, CBOR, Protocol Buffers, and Avro encode the same data in roughly 30–70 percent of the bytes and decode several times faster, because they store lengths and types explicitly instead of making the parser scan text for them. The cost is tooling: schemas, code generation, and files you can't open and read.

Draw the boundary between edges and interiors. Use JSON at the edges (public APIs, configuration, anything a human might open) and binary formats on internal high-volume paths: service fan-out, caches, event buses, anywhere large payloads move at high rates. If you already validate against schemas, Protobuf and Avro give you the schema and the speed in one move.

FAQ

How big is too big for JSON.parse?
JSON.parse has no hard limit, only symptoms. A few megabytes parse in single-digit milliseconds; tens of megabytes block the main thread noticeably in browsers; at hundreds of megabytes, stream, split, or convert the document. Memory usually binds first, since a parsed tree can take several times the text size, before parse time does.
Does minified JSON parse faster than pretty-printed JSON?
Slightly, but bandwidth gains matter more. A parser skips whitespace tokens cheaply, so minifying improves parse speed by a small percentage while cutting transfer size by 30–60 percent on typical documents. The rule: minify for the wire, format for humans in the editor.
Is YAML or XML faster to parse than JSON?
Usually not. YAML's grammar is far more complex and most YAML parsers run slower than their JSON equivalents; XML DOM parsing is comparable at best and more verbose on the wire. If you're switching formats for performance, jump to a binary encoding instead of another text format.
When is NDJSON the wrong choice?
When the data forms one document instead of a sequence of records: deeply cross-referenced, needing atomic writes, or consumed by tools that expect a single JSON value. NDJSON buys constant-memory processing and resumability at the price of no root envelope and no whole-file validity guarantee. For logs, queues, and exports, take that trade; for a config file, don't.

Try these tools

Related articles