Comparison
There are many JavaScript diffing libraries. Most were designed for JSON — plain objects, arrays, and primitives. @opentf/obj-diff is designed for real JavaScript values: Map, Set, Date, RegExp, TypedArray, ArrayBuffer, class instances, and circular references, with a patch() that replays a diff back onto the original.
Correctness across native types
The most important difference is not speed — it's whether a library notices a change at all. The table below is generated by the repository's accuracy benchmark (benchmarks/collect.mjs). ❌ means the library silently reported no difference for two values that were genuinely different — the most dangerous kind of wrong answer.
| Library | Maps | Sets | Dates | RegExps | TypedArray | Circular |
|---|---|---|---|---|---|---|
| obj-diff | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| microdiff | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| deep-diff | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| deep-diff-ts | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| deep-object-diff | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ |
| just-diff | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| @adobe/optimized-diff | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| recursive-diff | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
| jsondiffpatch | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
Map and Set are the common blind spot: change a value in a Map and most libraries report [].
Capabilities
| Capability | @opentf/obj-diff | Notes |
|---|---|---|
| Deep diffing at any depth | ✅ | |
patch() to reconstruct the target | ✅ | Round-trips: patch(a, diff(a, b)) deep-equals b. |
| Compact array diffs (Myers LCS) | ✅ | One op per real insert/remove — see Benchmarks. |
Native Map / Set / Date / TypedArray | ✅ | No "convert to JSON first" step. |
| Circular-reference safe | ✅ | |
Custom equality via diffWith() | ✅ | e.g. compare MongoDB ObjectIds by string. |
| TypeScript-native | ✅ | Full types, no @types package needed. |
| Move detection | ❌ | Reorders are expressed as remove + insert — see Caveats. |
| RFC 6902 (JSON Patch) wire format | ❌ | Intentional — see below. |
Type-safe serialization vs superjson
Diffing is only half the job — a diff usually has to travel. The same codec that powers serialize/deserialize (for diffs) is exposed as stringify/parse for any value — the same job as superjson: keep real JavaScript types alive across a JSON boundary. The tables below run stringify/parse against superjson v2.2.6 on identical values, generated by benchmarks/serialization.mjs.
Type fidelity
Round-trip a value, then check it comes back as the same type with the same data. ❌ is a silent loss (the value survives but wrong); 💥 throws.
| Value | obj-diff | superjson |
|---|---|---|
| Plain nested object | ✅ | ✅ |
| Date | ✅ | ✅ |
| Invalid Date | ✅ | ❌ |
| RegExp | ✅ | ✅ |
| Map | ✅ | ✅ |
| Set | ✅ | ✅ |
| BigInt | ✅ | ✅ |
| Int8Array | ✅ | ✅ |
| Float64Array [NaN,-0,∞] | ✅ | ❌ |
| BigInt64Array | ✅ | 💥 |
| ArrayBuffer | ✅ | ❌ |
| DataView | ✅ | ❌ |
| Error + custom prop | ✅ | ❌ |
| Boxed Number | ✅ | ❌ |
| Boxed String | ✅ | ❌ |
| undefined | ✅ | ✅ |
| NaN | ✅ | ✅ |
| -0 | ✅ | ✅ |
| Infinity | ✅ | ✅ |
| URL | ✅ | ✅ |
| Circular reference | ✅ | ✅ |
| Circular array | ✅ | ✅ |
| Round-trip score | 22/22 | 14/22 |
obj-diff restores every native type. superjson degrades Float64Array specials (NaN/-0/∞ → 0), turns ArrayBuffer/DataView into plain objects, unwraps boxed primitives, and drops an Error's own props — without throwing. Only BigInt64Array errors outright; the rest return a wrong value that flows straight into your app.
Wire size
| Value | obj-diff | superjson |
|---|---|---|
| Flat object | 41 B | 44 B |
| Mixed native types | 171 B | 153 B |
| Typed array (8) | 71 B | 96 B |
| Nested record | 192 B | 192 B |
| Tricky numbers | 132 B | 115 B |
| Circular graph | 67 B | 83 B |
Comparable overall: obj-diff is smaller for leaf and native-heavy values (no {json,meta} envelope), while superjson's single shared meta table wins when many special values cluster in one object.
Speed
| Library | stringify | parse | round trip |
|---|---|---|---|
| obj-diff | 8.17 µs | 4.08 µs | 13.3 µs |
| superjson | 24.9 µs | 5.75 µs | 27.7 µs |
obj-diff is consistently faster — superjson walks each value twice (once for json, once to record meta paths), where obj-diff encodes in a single pass. Absolute times are machine-dependent; the gap is the signal.
When to choose something else
obj-diff is not trying to be every diffing tool. Reach for an alternative when:
You need the interoperable RFC 6902 JSON Patch format (to send patches to a non-JS service, or apply them with a standard library) → use
fast-json-patchorrfc6902.obj-diffuses its own compact, JS-native op format instead; see the FAQ.Your arrays are large and frequently reordered and you need true move operations → a move-detecting differ will produce smaller diffs for shuffles.
obj-difftreats a move as a remove plus an insert.You only ever touch flat JSON and want the absolute lowest nanosecond count on tiny objects → a minimal positional differ like
microdiffshaves a few hundred nanoseconds. For anything with native types, collections, or where diff size matters,obj-diffwins on the axes that count.