How JSON.parse materializes entire documents in JavaScript, and how react-native-fast-json uses native simdjson + lazy JsonView access to change the cost model in React Native.
react-native-fast-json is an open-source React Native library that wraps simdjson in C++ and exposes a lazy JsonView through Nitro Modules.
Most teams treat JSON parsing as “cheap” because API responses are small. The mental model is:
bytes on wire → JSON.parse → plain objects in JS → app logic
That model breaks when the payload is hundreds of megabytes. The dominant cost is not “JSON syntax is slow” in the abstract, it is allocating and wiring up a full value tree in the JavaScript heap.
What JSON.parse actually does in React Native
When you call JSON.parse (or await response.json()), the engine:
- Holds the UTF-16/UTF-8 string (or an internal representation of the body)
- Walks the entire document
- For every object, array, number, boolean, and string, creates JavaScript values with engine overhead (hidden classes, property storage, array backing stores, interned strings, etc.)
For a ~250 MB file, outcomes depend heavily on platform and available RAM:
| Symptom | Typical cause |
|---|---|
| Multi-second freezes | Single-threaded parse + massive allocation on the JS thread |
| Memory far above file size | JS object graph overhead (often ~2–4× the raw JSON size in bad cases) |
| OOM / process kill | Peak JS heap + retained tree + rest of the app |
On the data_250mb.json fixture in our tests: iOS could complete JSON.parse but only with extreme CPU and memory spikes; Android always crashed before parse finished, making JSON.parse impossible for that payload on Android, while native parseFile worked on both platforms.
Benchmark fixture (source)
Measurements in this repo use the public ~250 MB JSON from antonmedv/json-examples:
| Repository | https://github.com/antonmedv/json-examples |
| File | data_250mb.json |
| Raw URL | https://github.com/antonmedv/json-examples/raw/refs/heads/master/data_250mb.json |
The example app downloads that URL, caches it on disk, then calls parseFile on the local path.
Measured results (parseFile, release builds, physical devices)
| Platform | Time to root JsonView
|
CPU / memory (observed) |
|---|---|---|
| iOS | ~100 ms | Stable, no long CPU peg, no runaway JS heap growth during parse |
| Android | ~500 ms | Stable, same qualitative behavior as iOS, slower wall time |
JSON.parse / response.json() on the same fixture:
| Platform | JSON.parse |
parseFile + lazy JsonView
|
|---|---|---|
| iOS | Many seconds; extreme CPU and memory spikes (~1.2 GB JS heap ballpark) | ~100 ms; CPU and memory stable |
| Android | Crash, could not complete parse | ~500 ms; CPU and memory stable (~400 MB mostly native buffer) |
The second row is not “free memory.” The file still occupies roughly file size + simdjson padding in native memory. The win is avoiding a second full copy as a giant JS object graph until you explicitly materialize pieces.
Why “read file as string, then JSON.parse” can be worse at peak
A common variant:
const text = await readFile(path, 'utf8');
const data = JSON.parse(text);
At peak you may briefly hold:
- The entire file as one JS string
- The entire parsed tree
- Parser scratch allocations
response.json() can sometimes avoid exposing one giant application-level string, but the end state is still the same: a full tree in JS. For large files, peak heap is the killer.
The technique: how react-native-fast-json does it
react-native-fast-json is what makes the native lazy model practical in React Native. Instead of returning a plain object from JSON.parse, it shifts work across a different boundary:
file bytes → native buffer (simdjson) → JsonView handle → read paths on demand
1. Native buffer (simdjson)
parseFile loads the file into a padded native buffer (simdjson::padded_string::load). Parsing runs in C++ with simdjson (SIMD-accelerated, widely used for large JSON).
The document’s canonical bytes live in native memory, not as nested JS objects.
2. Lazy JsonView (Nitro hybrid object)
The root returned to JavaScript is a JsonView, a Nitro hybrid object backed by C++. Calling:
getValue('metadata')atPath('$.a.b.c')-
asString()on a scalar
…does targeted native work and only crosses the bridge with small results (another JsonView handle or a primitive), not the whole document.
3. Materialization is explicit and expensive
When you call:
-
asObject(), subtree becomes a JS-friendly map/array structure -
rawJson(), subtree becomes a JS string
…you have chosen to pay the JS heap cost for that slice. On a 250 MB document, calling these on the root is equivalent to opting back into the problem you avoided.
Architecture
HybridFastJson (cpp/HybridFastJson.cpp):
-
parseFile(path), load file, constructHybridJsonView, cache by path string -
parseString(str), copy string into native view (not path-cached) -
release(path), erase cache entry for that path
HybridJsonView, navigation, path helpers, scalar coercion, optional materialization.
Until release(path), a cached parseFile keeps the full native buffer alive even if JS drops its JsonView reference, plan releases when the flow ends.
What you still pay for (honest accounting)
| Cost | Still there? |
|---|---|
| File-sized native memory | Yes, roughly JSON size + padding |
| Parse CPU | Yes, but native simdjson is much faster than building a JS tree |
| JS heap for the whole tree |
No, unless you asObject() / rawJson() large parts |
| Bridge traffic | Per access; keep reads coarse-grained for huge docs |
| Path cache |
parseFile retains buffer until release(path)
|
There is no zero memory overhead. The claim that holds up is: near-zero additional JS heap for the full document while you navigate lazily.
Why milliseconds (or low hundreds of ms) are plausible for hundreds of MB
simdjson is designed for throughput on large valid JSON. On the data_250mb.json fixture, parseFile after the file is on disk landed at ~100 ms on iOS and ~500 ms on Android in our release builds, with stable CPU and memory. By contrast, JSON.parse on iOS took many seconds with extreme spikes; on Android it always crashed, so native parse was the only viable option there.
Caveats when you reproduce benchmarks:
- Fixture, use the same public file or disclose yours
- Cold vs warm storage (first read after download vs re-parse)
- Platform, Android was ~5× slower than iOS in our tests; do not assume one number for all devices
- Device tier and storage speed
- JSON shape (depth, string density) affects constant factors
- Debug vs release native builds, ship measurements on release binaries
Comparison table (same fixture, measured + observed)
| Dimension |
JSON.parse (our tests) |
Native JsonView (parseFile) |
|---|---|---|
| iOS | Completes after many seconds; extreme CPU/memory spikes | ~100 ms; stable CPU/memory |
| Android | Crash, parse not possible on ~250 MB | ~500 ms; stable CPU/memory |
| Primary memory | Full JS object graph (when it survives) | Native padded buffer (~file size) |
| UI thread risk | High during parse | Lower; still avoid huge sync work in JS |
| Best access pattern | Whole-tree mutation | Few paths / scalars / wildcards |
| Foot-guns | Retaining whole data
|
asObject() / rawJson() on huge nodes; never calling release
|
Wildcards and paths
-
atPath('$.a.b.c'), simple dotted segments, no[index]in the path string. -
atPathWithWildcard('$.items[*].id'), dynamic segments; returnsstring[] | nullfor matched scalar strings.
For very large documents, prefer narrow paths over scanning wildcards across the whole tree unless you have measured the cost.
When native lazy parsing is the wrong tool
Not a common use case, prefer better alternatives
Shipping ~250 MB JSON to a phone is a smell, not a pattern. We benchmark it because it stress-tests the parser and matches real legacy pain, not because every app should do it.
Try these first (usually cheaper than optimizing parse):
- Don’t send the file, paginate, stream, or return only fields the UI needs.
- Don’t use JSON on the wire, schema-driven binary formats decode faster and smaller.
- Don’t keep one blob, SQLite (or similar) with indexes for the queries you actually run.
- Don’t parse on the critical path, generate summaries or shards on the server; sync deltas.
Native lazy parsing is a mitigation when you are locked into large JSON. It is not a substitute for a sane data contract.
When this library still does not help enough
- Small payloads,
JSON.parseis simpler and fast enough. - You need the entire document in JS anyway, you will materialize everything eventually; measure whether native parse + bulk
asObject()is still a win. - Heavy in-place mutation of arbitrary subtrees in JS, work on plain objects.
- You cannot hold file-sized native RAM (low-RAM devices + multiple huge files), budget and serialize access; even native buffers are ~file size.
Benchmark methodology
import { fastJson } from 'react-native-fast-json';
const root = await fastJson.parseFile('/path/to/large.json');
const version = root?.getValue('metadata')?.getValue('version')?.asString();
fastJson.release('/path/to/large.json');
-
Fixture,
data_250mb.jsonfrom antonmedv/json-examples, viahttps://github.com/antonmedv/json-examples/raw/refs/heads/master/data_250mb.json - RN Version 0.85.0, New Architecture.
- Device, physical iOS and Android (document model, OS, RN version, Hermes on/off).
- Build, release native binary.
-
Scenarios
- A:
response.json()orJSON.parseafter full string in memory - B: download/cache to disk →
parseFile(localPath)→ read N fields →release(path)
- A:
-
Metrics, wall time to root
JsonView, JS heap snapshot, CPU/memory stability in Instruments / Android Studio. -
Our results,
parseFile: iOS ~100 ms, Android ~500 ms, CPU/memory stable.JSON.parse: iOS many seconds with extreme spikes; Android always crashed on this fixture.
The example app implements download + parseFile + timing UI, use it as a starting point, not as universal truth.
Summary
| Idea | Detail |
|---|---|
| Problem |
JSON.parse materializes the whole document in the JS heap. |
| Technique | Parse in native with simdjson; expose JsonView; read lazily. |
| Speed |
data_250mb.json: ~100 ms (iOS) / ~500 ms (Android) for parseFile in our tests. |
| CPU / memory |
Stable on native parse; JSON.parse had extreme spikes on iOS and crashed on Android for this fixture. |
| Memory model | Avoid blowing up the JS heap; native still holds ~file size. |
| Discipline |
release(path), avoid huge asObject() / rawJson(), extract scalars to plain JS when done. |
Try react-native-fast-json
yarn add react-native-fast-json react-native-nitro-modules
Install pods, rebuild, then use fastJson.parseFile for large files on disk. Peer dependency: react-native-nitro-modules.
Top comments (0)