I wrote simdjson_dart because jsonDecode builds the whole document into Dart objects, even when I only need a few fields out of a big one. Fetch a 9 MB API response, read three values, and jsonDecode still turns the whole thing into objects you drop a line later.
That is work you pay for and throw away.
Here is the alternative. Same three fields, but the parser only builds the values you ask for.
final doc = SimdJsonDocument.parseBytes(bytes);
try {
final name = doc.at('/items/0/name') as String?;
final price = doc.at('/items/20000/price') as double?;
final tags = doc.at('/items/5/tags') as List?;
} finally {
doc.close();
}
Reading those three values out of a document that size, this runs about 10x faster than dart:convert doing the same work. The exact number depends on your data, and I will give the honest range below.
What it is
simdjson_dart is a thin layer over simdjson, the C++ JSON parser, reached through dart:ffi. It vendors simdjson 4.6.4.
There is nothing to install and no build file to write. The native code compiles at build time through Dart's build hooks, so you need Dart 3.10 or newer. It is on pub.dev now at 0.1.2.
The two ways to call it
The first is SimdJsonDocument. It parses once and builds a Dart value only for the fields you read. You address fields with an RFC 6901 JSON Pointer, so /items/0/name walks into the object the way you would expect. The empty string returns the whole document, and a missing path returns null. When you are done, call close() to free the native document.
The second is a drop-in for jsonDecode that starts from bytes:
final data = simdJsonDecodeBytes(bytes) as Map<String, dynamic>;
simdJsonDecodeBytes decodes the whole document into ordinary Dart values: Map<String, dynamic>, List, String, int, double, bool, and null. If you already call jsonDecode somewhere, this returns the same shapes.
Why the selective reads are cheap
parseBytes runs simdjson over the raw bytes and builds an index of where every value sits. That pass does touch the whole document, but it does not build Dart objects. When you call .at, it walks the index straight to that one spot and builds a Dart value for it. Ask for three fields and you build three values. The rest of the document stays as bytes.
The scan over the bytes is fast. The expensive part is allocating a Dart object for every value and linking them into a map-and-list tree. jsonDecode does that for the whole document, whether you read one field or all of them. SimdJsonDocument does the allocation only for the fields you actually read.
The numbers
Medians from bench/bench.dart, on an Apple Silicon MacBook, macOS arm64, Dart 3.11. The baseline is dart:convert doing the same work on the same input.
Reading 3 values out of a 6.7 to 9.2 MB document, SimdJsonDocument against dart:convert:
| Document shape | Speedup |
|---|---|
| API-like objects | 10.3x |
| Number-heavy arrays | 5.4x |
| String-heavy | 14.8x |
The string-heavy case wins the most because dart:convert builds the most objects there, and those are exactly the objects simdjson_dart never builds.
Full decode from bytes is a much smaller win. simdJsonDecodeBytes against jsonDecode:
| Document shape | Speedup |
|---|---|
| API-like objects | 1.19x |
| Number-heavy arrays | 1.75x |
| String-heavy | 1.21x |
The full-decode gains are modest and best on number-heavy data. dart:convert has a slow path for parsing numbers (tracked in dart-lang/sdk#55522), and that is most of what the number-heavy row is measuring. If you are going to decode the whole document anyway, this package is only worth reaching for on number-heavy data.
The one thing to get right: close()
The easy mistake is to forget close():
// WRONG: no close(), the native document stays alive
final doc = SimdJsonDocument.parseBytes(bytes);
final name = doc.at('/items/0/name') as String?;
return name; // doc is never freed
The parsed document lives in native memory, about the size of the input. The Dart heap does not count it and the GC does not see it, so nothing in your Dart memory graph looks wrong. Run that over a few hundred files and RSS climbs while the Dart heap stays flat.
There is a finalizer that frees documents you forget, but it runs whenever the GC gets around to it, not when you stopped reading. Treat it as a safety net, not a plan. The fix is the try / finally from the top of the post.
The honest limits
Two things I put in the README instead of hiding.
The first is speed, and it cuts the other way. If your input is already a Dart String and you decode all of it, plain jsonDecode is often faster than this package. The Dart VM decodes its UTF-16 strings directly. simdjson needs UTF-8 bytes, so you pay to convert the string before you parse it. The wins here are selective reads and inputs that already arrive as bytes. If you are holding a String and you need all of it, stay on dart:convert.
The second is strictness. simdjson validates hard, so it rejects a few inputs jsonDecode quietly accepts:
-
1e999, wherejsonDecodereturnsInfinity - integers past unsigned 64-bit
- lone surrogate escapes such as
"\ud800" - nesting deeper than 1024 levels
- documents over 4 GB
If your JSON can contain any of those, this parser will reject it rather than coerce it. For most API payloads that never comes up. For a few sources it will, and you want to know before you switch.
The numbers above are from macOS arm64. CI runs on Linux, macOS, and Windows. Flutter support is waiting on build hooks reaching stable Flutter, so for now this is server-side and CLI Dart.
Try it on your own data
The package is simdjson_dart on pub.dev, currently 0.1.2: pub.dev/packages/simdjson_dart. The source is at github.com/Yusufihsangorgel/simdjson_dart.
Before you switch anything, clone it and run bench/bench.dart against your own payloads. My numbers are my data on my machine, and the win depends mostly on how much of each document you actually read.




Top comments (0)