JSON Toolkit

Speeding up large JSON parsing

Bottom line: Optimising in the wrong order makes every library swap useless. Measure network, parse and render separately, then cut parses, shrink payloads and stream.

Intermediate~5 min readPublished 2026-10-30Updated 2026-10-30
The order in which to optimise large JSON parsing

Bottom line: measure network, parse and render separately first — most "slow parsing" turns out to be slow rendering — then climb the ladder in order: fewer parses, smaller payloads, streaming, a worker, and a binary format last.

Do not start by swapping libraries. Add one timer and you will usually find the render is the problem.

Step one: measure the stages

const t0 = performance.now();
const response = await fetch(url);
const text = await response.text();
const t1 = performance.now();
const data = JSON.parse(text);
const t2 = performance.now();
render(data);
const t3 = performance.now();
Dominant stage Real problem Fix
Network Payload too large Field selection, pagination, compression
JSON.parse Genuinely large input Fewer parses, streaming, worker
Render Tens of thousands of nodes at once Virtual list, paging, collapsing

Modern browsers parse 1MB in roughly 5–15ms and 10MB in 80–150ms. If 10MB takes two seconds, JSON.parse is not your bottleneck.

Step two: parse less

The cheapest win, and the most overlooked.

  • Stop parsing the same payload repeatedly. Ten components each calling JSON.parse(localStorage.getItem(...)) is ten parses. Cache once at module level.
  • Stop using JSON.parse(JSON.stringify(x)) for deep copies. It costs more than twice the data volume; structuredClone does not.
  • Do not parse on the main thread and then send the object to a worker. postMessage clones it again. Send the raw text and let the worker parse.
  • Ask for fewer fields. If the UI needs three properties, the API should not send three hundred.

Step three: shrink the payload

Pagination by page/size, cursors instead of deep offset, summary fields in list endpoints. This usually beats every frontend optimisation combined, because it reduces network, parse and memory at once.

Step four: stream

When the data must be processed as a whole and genuinely is large:

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

const pipeline = fs.createReadStream('big.json').pipe(parser()).pipe(streamArray());
for await (const { value } of pipeline) {
  await handleOne(value);
}

More code, and no random access — only suitable for record-by-record processing.

Step five: move it off the main thread

Parsing is fast but blocking. Put it in a worker and pass the text, not the parsed object, or structured cloning doubles the cost.

Step six: change the format, last

For internal traffic that is consistently tens of megabytes, NDJSON streams naturally, and Protobuf or MessagePack are smaller and faster. Both are protocol changes requiring coordination.

Do not

  • eval JSON: unsafe and not faster
  • Stringify JSON to embed it in JSON: doubles the size
  • Parse hundreds of thousands of tiny strings in a loop: batch them

FAQ

How do I know whether parsing or rendering is the bottleneck?

Time the request, the text read, JSON.parse and the render separately with performance.now(), and treat the largest stage as the real bottleneck. Modern browsers parse 1MB in roughly 5–15ms and 10MB in 80–150ms, so a 10MB payload that takes two seconds is rarely a JSON.parse problem.

Why does swapping libraries usually not help?

JSON.parse is already native and fast; most "slow parsing" comes from repeated parsing or rendering tens of thousands of nodes at once. A library swap buys 10–30%, while removing one full parse often saves half.

Should I move parsing into a Web Worker?

Move it into a worker when parsing blocks the main thread and the page freezes or animations drop frames. Pass the raw text rather than the parsed object, otherwise postMessage structured cloning doubles the cost.

Further reading