Superdiff provides a rich and readable diff for arrays, objects and texts. It supports stream and file inputs for handling large datasets efficiently, is battle-tested, has zero dependencies, and offers a top-tier performance.
ℹ️ The documentation is also available on our website!
Superdiff exports 4 functions:
Superdiff smoothly comparing 1.8 million fictional votes in real time using streamListDiff in the browser
| Feature | superdiff | deep-object-diff | deep-diff | diff | microdiff |
|---|---|---|---|---|---|
| Object diff | ✅ | ✅ | ✅ | ❌ | ✅ |
| List diff | ✅ | ❌ | ❌ | ||
| Text diff | ✅ | ❌ | ✅ | ✅ | ❌ |
| Streaming for huge datasets | ✅ | ❌ | ❌ | ❌ | ❌ |
| Move detection | ✅ | ❌ | ❌ | ❌ | ❌ |
| Output refinement | ✅ | ❌ | ❌ | ❌ | ❌ |
| Zero dependencies | ✅ | ✅ | ❌ | ✅ | ✅ |
Some libraries perform array diffing using index-by-index comparison. This approach cannot reliably detect insertions, deletions, or moves correctly. In those cases, support is marked as partial.
Environment: Node.js 24.12.0 (LTS) • MacBook Pro M2 (2023, Sequoia 15.1) • 16GB RAM.
Method: Warm up runs, then each script is executed 20 times, and we keep the median time. To minimize garbage collection and cross‑benchmark interference, all scenarios are run individually. All benchmark scripts are included so you can reproduce the results locally.
| Scenario | superdiff | arr-diff | deep-diff |
|---|---|---|---|
| 10k items array | 1.84 ms | 32.95 ms | 4.74 ms |
| 100k items array | 17.43 ms | 3363.15 ms | 50.36 ms |
| Scenario | superdiff | deep-object-diff | deep-diff | microdiff |
|---|---|---|---|---|
| 10k flat object keys | 2.27 ms | 2.44 ms | 39.37 ms | 2.24 ms |
| 100k flat object keys | 29.23 ms | 31.86 ms | 3784.50 ms | 29.51 ms |
| 100k nested nodes | 4.25 ms | 9.67 ms | 16.51 ms | 7.26 ms |
| Scenario | superdiff | diff |
|---|---|---|
| 10k words | 1.38 ms | 3.86 ms |
| 100k words | 21.68 ms | 45.93 ms |
| 10k sentences | 2.30 ms | 5.61 ms |
| 100k sentences | 21.95 ms | 62.03 ms |
(Superdiff uses its normal accuracy settings to match diff's behavior)
👉 Despite providing a full structural diff with a richer output, Superdiff consistently outperforms or matches the fastest diff libraries. It also scales linearly, even with deeply nested data.
I am grateful to the generous donors of Superdiff!
If you or your company uses this library, please show your support by becoming a sponsor! Your name and company logo will be displayed on the README.md. Premium support is also available.
import { getObjectDiff } from "@donedeal0/superdiff";Compares two objects and returns a diff for each value and its possible subvalues. Supports deeply nested objects of any value type.
Input
prevData: Record<string, unknown>;
nextData: Record<string, unknown>;
options?: {
ignoreArrayOrder?: boolean, // false by default,
showOnly?: {
statuses: ("added" | "deleted" | "updated" | "equal")[], // [] by default
granularity?: "basic" | "deep" // "basic" by default
}
}prevData: the original object.nextData: the new object.options-
ignoreArrayOrder: iftrue,["hello", "world"]and["world", "hello"]are consideredequal, because the two arrays contain the same values, just in a different order. -
showOnly: returns only the values whose status you are interested in. It takes two parameters:statuses: status you want to see in the output (e.g.["added", "equal"])granularity:basicreturns only the main keys whose status matches your query.deepcan return main keys if some of their nested keys' status match your request. The nested keys are filtered accordingly.
-
Output
type ObjectDiff = {
type: "object";
status: "added" | "deleted" | "equal" | "updated";
diff: Diff[];
};
type Diff = {
key: string;
value: unknown;
previousValue: unknown;
status: "added" | "deleted" | "equal" | "updated";
// recursive diff in case of nested keys
diff?: Diff[];
};Input
getObjectDiff(
{
id: 54,
user: {
name: "joe",
- member: true,
- hobbies: ["golf", "football"],
age: 66,
},
},
{
id: 54,
user: {
name: "joe",
+ member: false,
+ hobbies: ["golf", "chess"],
age: 66,
},
}
);Output
{
type: "object",
+ status: "updated",
diff: [
{
key: "id",
value: 54,
previousValue: 54,
status: "equal",
},
{
key: "user",
value: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
age: 66,
},
previousValue: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
+ status: "updated",
diff: [
{
key: "name",
value: "joe",
previousValue: "joe",
status: "equal",
},
+ {
+ key: "member",
+ value: false,
+ previousValue: true,
+ status: "updated",
+ },
+ {
+ key: "hobbies",
+ value: ["golf", "chess"],
+ previousValue: ["golf", "football"],
+ status: "updated",
+ },
{
key: "age",
value: 66,
previousValue: 66,
status: "equal",
},
],
},
],
}import { getListDiff } from "@donedeal0/superdiff";Compares two arrays and returns a diff for each entry. Supports duplicate values, primitive values and objects.
Input
prevList: T[];
nextList: T[];
options?: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
referenceKey?: string, // "" by default
ignoreArrayOrder?: boolean, // false by default,
considerMoveAsUpdate?: boolean // false by default
}prevList: the original list.nextList: the new list.optionsshowOnlygives you the option to return only the values whose status you are interested in (e.g.["added", "equal"]).referenceKeywill consider an object to beupdatedrather thanaddedordeletedif one of its keys remains stable, such as itsid. This option has no effect on other datatypes.ignoreArrayOrder: iftrue,["hello", "world"]and["world", "hello"]are consideredequal, because the two arrays contain the same values, just in a different order.considerMoveAsUpdate: iftrue, amovedvalue is consideredupdated.
Output
type ListDiff = {
type: "list";
status: "added" | "deleted" | "equal" | "moved" | "updated";
diff: {
value: unknown;
index: number | null;
previousIndex: number | null;
status: "added" | "deleted" | "equal" | "moved" | "updated";
}[];
};Input
getListDiff(
- ["mbappe", "mendes", "verratti", "ruiz"],
+ ["mbappe", "messi", "ruiz"]
);Output
{
type: "list",
+ status: "updated",
diff: [
{
value: "mbappe",
index: 0,
previousIndex: 0,
status: "equal",
},
- {
- value: "mendes",
- index: null,
- previousIndex: 1,
- status: "deleted",
- },
- {
- value: "verratti",
- index: null,
- previousIndex: 2,
- status: "deleted",
- },
+ {
+ value: "messi",
+ index: 1,
+ previousIndex: null,
+ status: "added",
+ },
+ {
+ value: "ruiz",
+ index: 2,
+ previousIndex: 3,
+ status: "moved",
},
],
}// If you are in a server environment
import { streamListDiff } from "@donedeal0/superdiff/server";
// If you are in a browser environment
import { streamListDiff } from "@donedeal0/superdiff/client";Streams the diff of two object lists, ideal for large lists and maximum performance.
ℹ️ streamListDiff requires ESM support for browser usage. It will work out of the box if you use a modern bundler (Webpack, Rollup) or JavaScript framework (Next.js, Vue.js).
Input
In a server environment,
Readablerefers to Node.js streams, andFilePathrefers to the path of a file (e.g.,./list.json). Examples are provided in the #usage section below.
prevList: Readable | FilePath | Record<string, unknown>[],
nextList: Readable | FilePath | Record<string, unknown>[],
referenceKey: keyof Record<string, unknown>,
options: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
chunksSize?: number, // 0 by default
considerMoveAsUpdate?: boolean; // false by default
useWorker?: boolean; // true by default
showWarnings?: boolean; // true by default
}In a browser environment,
ReadableStreamrefers to the browser's streaming API, andFilerefers to an uploaded or local file. Examples are provided in the #usage section below.
prevList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
nextList: ReadableStream<Record<string, unknown>> | File | Record<string, unknown>[],
referenceKey: keyof Record<string, unknown>,
options: {
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
chunksSize?: number, // 0 by default
considerMoveAsUpdate?: boolean; // false by default
useWorker?: boolean; // true by default
showWarnings?: boolean; // true by default
}prevList: the original object list.nextList: the new object list.referenceKey: a key common to all objects in your lists (e.g.id).optionschunksSizethe number of object diffs returned by each streamed chunk. (e.g.0= 1 object diff per chunk,10= 10 object diffs per chunk).showOnlygives you the option to return only the values whose status you are interested in (e.g.["added", "equal"]).considerMoveAsUpdate: iftrueamovedvalue is consideredupdated.useWorker: iftrue, the diff will be run in a worker for maximum performance. Only recommended for large lists (e.g. +100,000 items).showWarnings: iftrue, potential warnings will be displayed in the console.
⚠️ Warning: using Readable streams may impact workers' performance since they need to be converted to arrays. Consider using arrays or files for optimal performance. Alternatively, you can turn theuseWorkeroption off.
Output
The objects diff are grouped into arrays - called chunks - and are consumed thanks to an event listener. You have access to 3 events:
data: to be notified when a new chunk of object diffs is available.finish: to be notified when the stream is finished.error: to be notified if an error occurs during the stream.
interface StreamListener<T> {
on(event: "data", listener: (chunk: StreamListDiff<T>[]) => void);
on(event: "finish", listener: () => void);
on(event: "error", listener: (error: Error) => void);
}
type StreamListDiff<T extends Record<string, unknown>> = {
value: T | null;
index: number | null;
previousValue: T | null;
previousIndex: number | null;
status: "added" | "deleted" | "moved" | "updated" | "equal";
};Input
You can send streams, file paths, or arrays as input:
If you are in a server environment
// for a simple array
const stream = [{ id: 1, name: "hello" }]
// for a large array
const stream = Readable.from(list, { objectMode: true });
// for a local file
const stream = path.resolve(__dirname, "./list.json");
If you are in a browser environment
// for a simple array
const stream = [{ id: 1, name: "hello" }]
// for a large array
const stream = new ReadableStream({
start(controller) {
list.forEach((value) => controller.enqueue(value));
controller.close();
},
});
// for a local file
const stream = new File([JSON.stringify(file)], "file.json", { type: "application/json" });
// for a file input
const stream = e.target.files[0]; Example
const diff = streamListDiff(
[
- { id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
{ id: 3, name: "Item 3" }
],
[
+ { id: 0, name: "Item 0" },
{ id: 2, name: "Item 2" },
+ { id: 3, name: "Item Three" },
],
"id",
{ chunksSize: 2 }
);Output
diff.on("data", (chunk) => {
// first chunk received (2 object diffs)
[
+ {
+ value: { id: 0, name: "Item 0" },
+ index: 0,
+ previousValue: null,
+ previousIndex: null,
+ status: "added"
+ },
- {
- value: null,
- index: null,
- previousValue: { id: 1, name: "Item 1" },
- previousIndex: 0,
- status: "deleted"
- }
]
// second chunk received (2 object diffs)
[
{
value: { id: 2, name: "Item 2" },
index: 1,
previousValue: { id: 2, name: "Item 2" },
previousIndex: 1,
status: "equal"
},
+ {
+ value: { id: 3, name: "Item Three" },
+ index: 2,
+ previousValue: { id: 3, name: "Item 3" },
+ previousIndex: 2,
+ status: "updated"
+ },
]
});
diff.on("finish", () => console.log("Your data has been processed. The full diff is available."))
diff.on("error", (err) => console.log(err))import { getTextDiff } from "@donedeal0/superdiff";Compares two texts and returns a structured diff at a character, word, or sentence level.
Input
previousText: string | null | undefined,
currentText: string | null | undefined,
options?: {
separation?: "character" | "word" | "sentence", // "word" by default
accuracy?: "normal" | "high", // "normal" by default
detectMoves?: boolean // false by default
ignoreCase?: boolean, // false by default
ignorePunctuation?: boolean, // false by default
locale?: Intl.Locale | string // undefined by default
}previousText: the original text.currentText: the current text.optionsseparationwhether you want acharacter,wordorsentencebased diff.accuracy:normal(default): fastest mode, simple tokenization.high: slower but exact tokenization. Handles all language subtleties (Unicode, emoji, CJK scripts, locale‑aware segmentation when a locale is provided).
detectMoves:false(default): optimized for readability. Token moves are ignored so insertions don’t cascade and break equality (recommended for UI diffing).true: semantically precise, but noiser — a single insertion shifts all following tokens, breaking equality.
ignoreCase: iftrue,helloandHELLOare considered equal.ignorePunctuation: iftrue,hello!andhelloare considered equal.locale: the locale of your text. Enables locale‑aware segmentation in high accuracy mode.
Output
type TextDiff = {
type: "text";
status: "added" | "deleted" | "equal" | "updated";
diff: {
value: string;
index: number | null;
previousValue?: string;
previousIndex: number | null;
status: "added" | "deleted" | "equal" | "moved" | "updated";
}[];
};WITHOUT MOVES DETECTION
This is the default output. Token moves are ignored so insertions don’t cascade and break equality. Updates are rendered as two entries (added + deleted). The algorithm uses longest common subsequence (LCS), similar to GitHub diffs.
Input
getTextDiff(
- "The brown fox jumped high",
+ "The orange cat has jumped",
{ detectMoves: false, separation: "word" }
);Output
{
type: "text",
+ status: "updated",
diff: [
{
value: 'The',
index: 0,
previousIndex: 0,
status: 'equal',
},
- {
- value: "brown",
- index: null,
- previousIndex: 1,
- status: "deleted",
- },
- {
- value: "fox",
- index: null,
- previousIndex: 2,
- status: "deleted",
- },
+ {
+ value: "orange",
+ index: 1,
+ previousIndex: null,
+ status: "added",
+ },
+ {
+ value: "cat",
+ index: 2,
+ previousIndex: null,
+ status: "added",
+ },
+ {
+ value: "has",
+ index: 3,
+ previousIndex: null,
+ status: "added",
+ },
{
value: "jumped",
index: 4,
previousIndex: 3,
status: "equal",
},
- {
- value: "high",
- index: null,
- previousIndex: 4,
- status: "deleted",
- }
],
}WITH MOVE DETECTION
If you prefer a semantically precise diff, activate the detectMoves option. Direct token swaps are considered updated.
Input
getTextDiff(
- "The brown fox jumped high",
+ "The orange cat has jumped",
{ detectMoves: true, separation: "word" }
);Output
{
type: "text",
+ status: "updated",
diff: [
{
value: 'The',
index: 0,
previousIndex: 0,
status: 'equal',
},
+ {
+ value: "orange",
+ index: 1,
+ previousValue: "brown",
+ previousIndex: null,
+ status: "updated",
+ },
+ {
+ value: "cat",
+ index: 2,
+ previousValue: "fox",
+ previousIndex: null,
+ status: "updated",
+ },
+ {
+ value: "has",
+ index: 3,
+ previousIndex: null,
+ status: "added",
+ },
+ {
+ value: "jumped",
+ index: 4,
+ previousIndex: 3,
+ status: "moved",
+ },
- {
- value: "high",
- index: null,
- previousIndex: 4,
- status: "deleted",
- }
],
}DoneDeal0
Issues and pull requests are welcome!
Superdiff is safe to recommend for fast, structured diffs on arrays, objects and texts.



