diff review
diff 9.0.0, also known as jsdiff, finds insertions and deletions between strings or token arrays with a Myers edit algorithm. Callers choose whether a token means a Unicode code point, word, line, sentence, CSS token, JSON line, or array item, then receive ordered change objects. The same package creates, parses, reverses, formats, and applies unified patches. Version 9 adds Git extended-header support for creations, deletions, renames, copies, modes, and binary markers, and it correctly quotes unusual filenames. It also drops ES5 targets.
diff 9.0.0 installed in 0.4 seconds with 0 direct dependencies and produced an 8.1 KB gzipped browser bundle in our sandbox, with 0 audit findings. Install it for text spans or unified-patch work, but put work limits around unrelated inputs and build your own safe filesystem layer for Git patches.
We installed it
| Install | ✓ · 0.4s | 3 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 8.1 KB | gzipped (22.5 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does diff install cleanly?
Yes. In a fresh container with an empty cache, npm install diff finished in 0.4s, leaving 3 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does diff add to a browser bundle?
8.1 KB gzipped (22.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does diff work with both ESM and CommonJS?
Yes. Both import 'diff' and require('diff') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does diff include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
diff or fast-diff: which should you use?
fast-diff: Use it for a compact character-level diff when patches and multiple tokenizers are unnecessary. diff 9.0.0 installed in 0.4 seconds with 0 direct dependencies and produced an 8.1 KB gzipped browser bundle in our sandbox, with 0 audit findings.
When should you not use diff?
Readers expect semantic cleanup that groups a noisy minimal edit into a human editorial change. jsdiff reports token edits and does not rewrite them for readability.
Use it if
- A review screen, editor, assertion failure, or history view needs added, removed, and unchanged text spans.
- JavaScript must create or consume unified patches without invoking the system `diff` and `patch` commands.
- Git patch metadata such as rename, copy, mode, create, and delete headers must survive parsing or formatting.
- Untrusted comparisons need an explicit `timeout` or `maxEditLength` rather than unlimited edit-distance work.
- Readers expect semantic cleanup that groups a noisy minimal edit into a human editorial change. jsdiff reports token edits and does not rewrite them for readability.
- Large unrelated inputs must finish synchronously on the browser main thread. Myers work grows with edit distance, so use limits, callback mode, or a worker.
- The runtime is an ES5 browser. Version 9 uses `TextDecoder`, `Uint8Array`, and an ES6 compiler target; the release notes direct ES5 users to version 8.
- The result must identify nested object paths and values. `diffJson` compares sorted, formatted JSON text rather than producing JSON Pointer operations.
- You intend to accept a Git patch and write it directly to disk. Parsing does not validate paths, perform renames, manage permissions, apply binary data, or roll back partial writes.
Setup reality
We installed diff 9.0.0 in a fresh Node 22 Bookworm sandbox in 0.4 seconds. The environment ended with 3 packages and 1 MB on disk. npm audit returned 0 known vulnerabilities across all severity levels. The package declares 0 direct and 0 peer dependencies, carries bundled TypeScript declarations, and worked through both require() and ESM import. Our esbuild browser test measured 22.5 KB minified and 8.1 KB gzipped.
There are no credentials or configuration files. The choice that changes results is the tokenizer. diffWords ignores whitespace while matching, diffWordsWithSpace preserves whitespace tokens, and diffLines exposes options for CRLF, trailing whitespace, and a missing final newline. An Intl.Segmenter improves word boundaries for languages such as Chinese, but native segmenters may differ across engines. Persisted diffs need one pinned segmenter implementation if repeatability matters.
Expensive comparisons need a ceiling. A synchronous call with timeout or maxEditLength returns undefined after giving up. Callback mode also returns undefined immediately and delivers its eventual result to the callback while yielding between work slices. TypeScript models those calls with separate overloads, so keep the options object typed instead of routing it through any. Browser UI code should still move large jobs off the main thread.
applyPatch returns false when a hunk cannot be placed; an empty string is a successful result, so a truthiness check is wrong. fuzzFactor relaxes surrounding context but never excuses a missing deletion or mismatched lines immediately beside an insertion. Version 9 understands Git headers, yet applyPatches delegates file loading and saving to callbacks. Path containment, binary changes, copy order, modes, atomic writes, and cleanup stay in your application.
Patterns
Inspect character changes compare-characters
import { diffChars } from 'diff';
for (const change of diffChars('cafe', 'café')) {
const kind = change.added ? 'added' : change.removed ? 'removed' : 'same';
console.log(kind, change.value);
}`diffChars` treats Unicode code points as tokens in version 9, and each change object carries explicit boolean flags.
Compare prose by word compare-words
import { diffWords } from 'diff';
const changes = diffWords(
'Deploy the blue service.',
'Deploy the green service.',
{ ignoreCase: true },
);`diffWords` ignores whitespace for matching; use `diffWordsWithSpace` when whitespace itself must appear as a version 9 change.
Use language-aware word boundaries segment-localized-words
import { diffWords } from 'diff';
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
const changes = diffWords('我喜欢蓝色', '我喜欢绿色', {
intlSegmenter: segmenter,
});Native `Intl.Segmenter` output can vary by engine; stored diffs need 1 fixed polyfill for reproducible token boundaries.
Normalize line-ending differences compare-lines
import { diffLines } from 'diff';
const changes = diffLines(windowsText, unixText, {
stripTrailingCr: true,
ignoreNewlineAtEof: true,
});`stripTrailingCr` removes trailing carriage returns before comparison, while `ignoreNewlineAtEof` covers only the final newline.
Match array records by id compare-arrays
import { diffArrays } from 'diff';
const before = [{ id: 1 }, { id: 2 }];
const after = [{ id: 1 }, { id: 3 }];
const changes = diffArrays(before, after, {
comparator: (left, right) => left.id === right.id,
});The comparator receives an old-array item first and a new-array item second, which matters when equality is directional.
Stop a costly comparison limit-diff-work
import { diffLines } from 'diff';
const result = diffLines(oldText, newText, {
timeout: 250,
maxEditLength: 10_000,
});
if (result === undefined) {
throw new Error('diff limit reached');
}A bounded version 9 call returns `undefined` when either limit ends the search; it does not return a partial change list.
Run comparison in callback mode yield-during-diff
import { diffLines } from 'diff';
diffLines(oldText, newText, {
timeout: 1_000,
callback(result) {
if (result === undefined) return console.error('timed out');
render(result);
},
});The function itself returns `undefined` in callback mode, then yields between work slices until the callback receives a result.
Generate a reviewable patch create-unified-patch
import { createTwoFilesPatch, FILE_HEADERS_ONLY } from 'diff';
const patch = createTwoFilesPatch(
'config.old', 'config.new', before, after, '', '',
{ context: 3, headerOptions: FILE_HEADERS_ONLY },
);Three context lines are included here; some patch consumers reject optional `Index:` and underline headers, so `FILE_HEADERS_ONLY` removes them.
Read Git extended headers parse-git-patch
import { parsePatch } from 'diff';
for (const file of parsePatch(gitOutput)) {
console.log({
old: file.oldFileName,
next: file.newFileName,
rename: file.isRename,
mode: file.newMode,
});
}Version 9 parses hunkless renames and mode changes, and filename fields may be `undefined` when no header identifies them.
Distinguish failure from empty output apply-single-patch
import { applyPatch } from 'diff';
const output = applyPatch(source, patch, { fuzzFactor: 0 });
if (output === false) {
throw new Error('patch did not match');
}
await save(output);Compare against `false` exactly because a patch that deletes all content succeeds with a 0-character string.
Invert a parsed patch reverse-patch
import { formatPatch, parsePatch, reversePatch } from 'diff';
const parsed = parsePatch(patchText);
const undoText = formatPatch(parsed.map((file) => reversePatch(file)));Git binary patch bodies are opaque to version 9; reversing their metadata does not create a usable inverse binary delta.
Render changes without injecting HTML escape-rendered-diff
import { diffWords } from 'diff';
const fragment = document.createDocumentFragment();
for (const change of diffWords(before, after)) {
const span = document.createElement('span');
span.className = change.added ? 'add' : change.removed ? 'remove' : 'same';
span.textContent = change.value;
fragment.append(span);
}
output.replaceChildren(fragment);Assigning `textContent` keeps the 1 compared string from becoming executable markup; jsdiff does not escape rendered output for you.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fast-diff | npm | Use it for a compact character-level diff when patches and multiple tokenizers are unnecessary. |
| deep-diff | npm | Use it when object paths and before-and-after values matter more than printable text. |
| jsondiffpatch | npm | Use it for reversible object deltas, array move detection, and an optional HTML renderer. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

