Library / SDK
AsyncBanana/microdiff avatar
AsyncBanana/microdiff

microdiff: a small object differ that returns structured change records

A fast, zero dependency object and array comparison library. Significantly faster than most other deep comparison libraries and has full TypeScript support.

3,872 stars84 forksJavaScriptMIT

At a glance

What is it?
A zero dependency deep comparison library whose whole API is one function returning CREATE, REMOVE and CHANGE entries, small enough to ship to a browser and typed out of the box.
Who is it for?
microdiff earns its place when you need the difference between two values as data rather than as a rendered view: change tracking in a form library, a dirty check before a re-render, or a structured log of what an update touched. The single exported function and the flat array of path-and-value records are the whole API, so the integration cost is close to zero.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 65 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole API is one function and one import

Installation is a single package with no transitive dependencies:

bash
npm i microdiff

The README then shows the entire usage pattern. You pass two values to `diff()` and get an array back:

js
import diff from "microdiff";

const obj1 = {
+	originalProperty: true,
+};
+const obj2 = {
+	originalProperty: true,
+	newProperty: "new",
+};
+
+console.log(diff(obj1, obj2));
+

There is no class to instantiate and no configuration object to construct. CommonJS users take the default export off the module object, and Deno users import from `https://deno.land/x/microdiff@VERSION/index.ts` with the version substituted. The `package.json` shows both module systems published side by side, with an `exports` map pointing `import` at one build and `require` at the other, so bundlers pick the right one without a configuration flag.

The package description is more precise than the README headline: small, fast, zero dependency deep object and array comparison. The type declarations ship alongside the JavaScript, which is why the README lists full TypeScript support as a feature rather than an afterthought.

What a diff record actually contains

The output is a flat array with three possible change types: `CREATE`, `REMOVE` and `CHANGE`. That is the entire contract, and the README documents each field. The `path` property is an array of keys walking one level deeper per element until it reaches the property that changed, and each element is a string or a number depending on whether the container is an array or an object.

One detail there catches people: objects with numeric keys still produce string elements in the path. So an array index arrives as a number and an object key that looks like `0` arrives as the string `"0"`. If you are walking paths in reverse to rebuild a value, you have to handle both.

The value fields differ by type. `value` exists on `CREATE` and `CHANGE` and holds the new value. `oldValue` exists on `CHANGE` and `REMOVE`. And the `path` points at a property in the new object, except on a `REMOVE`, where it points into the old object because that is where the property still lives. Getting that exception wrong is the most common way to misapply the output.

There is no nesting in the result. A deep change is one record with a long path, not a tree, which is convenient for logging and for feeding into a reducer, and inconvenient if you wanted a structural patch you could apply without reimplementing path walking.

Cycle handling is on by default and can be turned off

Circular references are the one behaviour that costs something at runtime, so the library makes it a switch. Cycles are supported by default, and the README explains that you can disable the detection when you know the input has no cycles, which is the normal case for parsed JSON:

js
diff(obj1, obj2, { cyclesFix: false });

The naming is worth pausing on. `cyclesFix` is not a boolean you need to set in the common case; it is a boolean you set to opt out of work you do not need. The benchmark table in the README makes the cost visible, reporting the no-cycles configuration as the baseline at 100% and the default cycle-aware path at 149%. Roughly half again the time for a guarantee against infinite recursion is a reasonable trade, and turning it off on trusted input recovers most of it.

The other place this library diverges from the naive comparison is rich types. The feature list names `new Date()` and `new RegExp()` support explicitly, and version 1.6.0 added handling for Temporal types. The changelog for that release also mentions switching rich type storage to an array and removing redundant checks, alongside a general pass on recursive diff performance.

The benchmark table, with the caveat the README attaches

The README publishes a geometric mean of time per operation relative to Microdiff without cycles, where 100% means equal time. Its own numbers put microdiff without cycles at the baseline, microdiff with cycles at 149%, deep-diff at 197%, deep-object-diff at 288% and jsDiff at 1565%. A faster library and a text diff library appearing in the same table is a little odd, and it is worth knowing that jsDiff does character and line diffing, which is a different job with different output.

More to the point, the README attaches its own warning. It states the results come from a suite matching real-world use cases across multiple open source repositories, run under Node 22.12.0 on a Ryzen 7950x at roughly 4.30 GHz, using mitata to reduce random variation. It then links to an article about the inherent errors in benchmarking JavaScript and says the results should be taken with a grain of salt, while pointing at `bench.js` for anyone who wants to run the comparison in their own runtime.

That combination is rarer than it should be. A project publishing numbers and then telling you to distrust them, and shipping the harness so you can check, is doing something different from the usual benchmark table. The claim to take seriously is the size one, not the speed one.

Where a structured differ beats the alternatives

The usual alternative is not a competing package but a different kind of tool. jsDiff and diff produce text patches, which is what you want when the input is a file and the consumer is a reviewer or a version control system. `jsondiffpatch` produces a patch document rather than a change list, which is the right shape when you want to apply the difference to another copy of the same structure. Microdiff produces a flat list of changes with paths and values, which is the right shape when the consumer is your own code and you want to react to what changed.

That distinction shows up in practice. A form library checking whether a model changed can walk the array and update only the fields named in each path. A cache can use the paths as keys to invalidate. A logger can record the array without a serializer. None of those need a patch that can be replayed against a third copy, and building one is where the extra weight would come from.

The zero dependency and sub-kilobyte claims matter most in the browser and in service workers, both of which the README names as supported environments. A differ you bundle into a client bundle costs you the dependency tree of whatever it pulls in; this one costs under a kilobyte and nothing else.

Building from source and the repository layout

The tree is short, which matches the size of the thing being built: `index.ts` is the implementation, `tests/` holds the suite, `bench.js` and `benchmarks/` hold the performance harness, and `package.json`, `tsconfig.json` and `.prettierrc` hold the tooling. There is no `src/` directory and no bundler configuration, so the whole library is one TypeScript file compiled twice.

The `package.json` scripts make that concrete. `npm run build` runs TypeScript twice, once emitting CommonJS and once emitting ES modules, moves the CommonJS output and its declaration file to the `.cjs` and `.d.cts` names Node needs, and formats the result with prettier. `npm run test` builds and then runs the suite under the Node test runner with `--harmony-temporal`, which is required for the Temporal support added in 1.6.0. `npm run bench` builds and runs `bench.js` with `--expose-gc`. There is even a `size` script that pipes terser output through gzip and counts the bytes, which is the honest way to keep a sub-kilobyte claim honest.

Only `dist` is published, and the package is MIT licensed. The last release was v1.6.0 on 2026-08-02 and the last push was on the same date, so the file that defines the performance characteristics is the one currently in the repository.

Editorial conclusion

microdiff earns its place when you need the difference between two values as data rather than as a rendered view: change tracking in a form library, a dirty check before a re-render, or a structured log of what an update touched. The single exported function and the flat array of path-and-value records are the whole API, so the integration cost is close to zero. It is the wrong choice for text diffing, where jsDiff or diff handles line and word granularity, and for object graphs so deep that recursion depth becomes a concern. The release dated 2026-08-02 added Temporal type handling and a recursive diff performance pass, which suggests the library is still being tuned against real input. Read the CREATE, REMOVE and CHANGE contract once before wiring it into a state store, because the REMOVE path points at the old object rather than the new one.

Frequently asked questions

What is microdiff in JavaScript?

It is a zero dependency library that compares two objects or arrays deeply and returns the differences as data. You call a single function, diff, with the two values and receive an array of records of type CREATE, REMOVE or CHANGE, each carrying a path and the relevant value.

How do I use microdiff with TypeScript?

Nothing special is required. Type declarations ship with the package, so an editor picks them up from the import itself. Import the default export and call diff with two values; the second argument is an options object where cyclesFix disables circular reference detection.

Is microdiff faster than deep-diff and jsDiff?

The README publishes a benchmark table putting microdiff below deep-diff, deep-object-diff and jsDiff, measured with mitata on Node 22.12.0. The same section says those numbers should be taken with a grain of salt because of the inherent errors in benchmarking JavaScript, and the repository ships bench.js so readers can run the comparison themselves.

Official sources

  1. AsyncBanana/microdiff on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/asyncbanana-microdiff.svg)](https://hysenlabs.com/projects/asyncbanana-microdiff)