diff
jsdiff (published on npm as diff) is a JavaScript implementation of the Myers diff algorithm. Give it two strings and it returns an array of change objects saying what was added, removed, or kept, tokenized by character, word, line, sentence, CSS token, or pretty-printed JSON. It also speaks unified diff format: it can create patches, parse them (including Git-style patches with renames and mode changes), apply them to text with configurable fuzz, and reverse them. Zero dependencies, works in Node and the browser, and it is the diff engine behind the red/green output in a lot of test runners and CLI tools.
The default answer for text diffing and unified patches in JavaScript, with an API that has earned its 137M weekly downloads. Adopt it freely in apps, but pin the major version and read the changelog before upgrading, because recent majors have not been shy about breaking changes.
Use it if
- You are building a text comparison UI (code review, document history, admin audit views); change objects map one-to-one to colored spans
- You need to generate, parse, or apply unified diff patches in JavaScript, including Git patches with rename, copy, and file mode headers
- You are diffing structured data: diffJson normalizes key order before diffing, and diffArrays takes a custom comparator for object tokens
- You need to diff on a server without blocking the event loop: callback mode plus the maxEditLength and timeout options let you bound or abort expensive diffs
- You need human-friendly diffs of prose at character level; jsdiff has no semantic cleanup pass, so noisy inputs produce fragmented diffs where diff-match-patch's cleanupSemantic gives readable ones
- You diff very large files on hot paths; the algorithm is O(ND) pure JavaScript, and worst-case inputs (two mostly different texts) get slow enough that you must reach for maxEditLength or timeout as an escape hatch
- You must support old browsers or old Node: v9 (April 2026) only targets Baseline widely-available JavaScript, so ES5-era environments need to stay on 8.x and backport fixes yourself
- You want a stable target to build a library on top of: there were four breaking major releases between September 2024 and April 2026 (6.0.0, 7.0.0, 8.0.0, 9.0.0), including tokenization behavior changes in diffWords, so pin carefully
Setup reality
npm install diff and you are running; zero dependencies, ESM and CommonJS both work, plus a dist/diff.js UMD build exposing a global Diff for script tags. Two traps: since v8 types are bundled, so uninstall @types/diff or the stale types will fight the real ones; and the TypeScript overloads for async/abortable modes only resolve when your options object is statically analyzable, so building options dynamically (options: any = {}) produces confusing type errors the README itself documents.
Patterns
Diff two strings character by characterdiff-characters
import { diffChars } from 'diff';
const changes = diffChars('beep boop', 'beep boob blah');
for (const part of changes) {
if (part.added) console.log('+', part.value);
else if (part.removed) console.log('-', part.value);
else console.log(' ', part.value);
}Change objects where added and removed are both false are common text. Characters means Unicode code points, so surrogate pairs are not split.
Diff by words, optionally case-insensitivediff-words
import { diffWords } from 'diff';
diffWords('The quick fox', 'the slow fox', { ignoreCase: true });
// better word splitting for non-English text:
const seg = new Intl.Segmenter('zh', { granularity: 'word' });
diffWords(oldZh, newZh, { intlSegmenter: seg });Whitespace is ignored when computing the diff but preserved in output. The default regex tokenizer is weak for CJK text; pass an Intl.Segmenter with granularity 'word' for those languages.
Diff line by linediff-lines
import { diffLines } from 'diff';
diffLines(oldText, newText, {
ignoreWhitespace: true, // trim before comparing lines
stripTrailingCr: true, // Windows vs Unix line endings
});newlineIsToken: true reads better for humans but is worse for patch output; leave it off when feeding results into patch creation. Combining it with ignoreWhitespace gives surprising results on blank lines.
Diff two JSON-serializable objectsdiff-json-objects
import { diffJson } from 'diff';
const changes = diffJson(
{ name: 'Ana', role: 'admin' },
{ role: 'viewer', name: 'Ana' }
);
// only the role line shows as changedObjects are serialized with alphabetically ordered keys first, so property order never produces false diffs. Use undefinedReplacement to control how undefined values serialize.
Diff arrays of objects with a custom comparatordiff-arrays-custom-equality
import { diffArrays } from 'diff';
const result = diffArrays(oldUsers, newUsers, {
comparator: (left, right) => left.id === right.id,
});Default equality is ===, which never matches two distinct objects; without a comparator every object counts as removed plus added.
Create a unified diff patch (like diff -u)create-unified-patch
import { createTwoFilesPatch } from 'diff';
const patch = createTwoFilesPatch(
'config.old.json', 'config.new.json',
oldStr, newStr,
undefined, undefined,
{ context: 3 }
);Set context to Infinity to include the whole file in one hunk. The headerOptions option controls the Index/underline/filename header lines if a consumer tool is picky about them.
Apply a unified diff patch to textapply-patch
import { applyPatch } from 'diff';
const patched = applyPatch(sourceText, patchString, { fuzzFactor: 2 });
if (patched === false) {
throw new Error('patch did not apply');
}Check === false, not falsy: a valid result can be an empty string. fuzzFactor allows mismatched context lines, but deleted lines must exist in the source regardless.
Parse a Git patch and detect renamesparse-git-patch
import { parsePatch } from 'diff';
const patches = parsePatch(gitDiffOutput);
for (const p of patches) {
if (p.isRename) console.log(`${p.oldFileName} -> ${p.newFileName}`);
if (p.isBinary) console.log('binary change, no hunks to apply');
}isGit, isRename, isCopy, isCreate, isDelete, oldMode, and newMode only appear on Git-style patches. Binary patches carry no hunks, so you cannot apply them from the patch text alone.
Give up early on huge or unrelated inputsbound-expensive-diffs
import { diffLines } from 'diff';
const result = diffLines(bigOld, bigNew, {
maxEditLength: 1000, // or: timeout: 200 (ms)
});
if (result === undefined) {
// too different; fall back to 'file changed' UI
}Both options make the function return undefined instead of a diff when the limit is hit; handle that case or you will crash on .forEach of undefined.
Compute a diff without blocking the event loopasync-diff-callback
import { diffLines } from 'diff';
diffLines(oldText, newText, {
callback: (changes) => {
render(changes);
},
});In callback mode the function returns undefined and delivers the result asynchronously. TypeScript picks the right overload only if the callback is declared inline in the options literal.
Build an undo patchreverse-patch
import { structuredPatch, reversePatch, formatPatch } from 'diff';
const patch = structuredPatch('a.txt', 'a.txt', oldStr, newStr);
const undo = reversePatch(patch);
const undoText = formatPatch(undo);Reversing Git patches with copy from/copy to headers cannot be done correctly from the patch alone; the output for those will usually be rejected by git apply.
Render a diff as colored HTMLrender-diff-html
import { diffWords } from 'diff';
const html = diffWords(oldStr, newStr)
.map((part) => {
const cls = part.added ? 'ins' : part.removed ? 'del' : 'same';
return `<span class="${cls}">${escapeHtml(part.value)}</span>`;
})
.join('');Escape part.value yourself; jsdiff returns raw text and building innerHTML from user content without escaping is an XSS bug.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| diff-match-patch | npm | You want semantic cleanup of noisy character diffs or Google-style fuzzy patch application |
| fast-diff | npm | You only need raw character-level diffs as fast as possible and no patch tooling |
| diff-sequences | npm | You want the bare Myers algorithm over arbitrary sequences (it is what Jest uses) and will build your own output layer |
| microdiff | npm | You are diffing object and array state, not text |