How to Diff Strings or Files in JavaScript
Comparing two pieces of text — a config file before and after, two API responses, a draft against an edit — is one of the most common tasks that has no built-in answer in JavaScript. There is no String.prototype.diff(). This guide covers the three approaches that cover every real-world case: a hand-rolled line-by-line walk for simple equality, the jsdiff library for production-grade diffs with multiple output formats, and Google's diff-match-patch for character-level precision. Test any approach against the diff checker tool to confirm the output.
Approach 1: Manual Line-by-Line
For simple cases where you only need to know which lines differ, you don't need a library. Split both strings into arrays of lines, then walk them in parallel:
function lineDiff(a, b) {
const linesA = a.split('\n');
const linesB = b.split('\n');
const max = Math.max(linesA.length, linesB.length);
const out = [];
for (let i = 0; i < max; i++) {
if (linesA[i] === linesB[i]) {
out.push({ type: 'unchanged', line: linesA[i] });
} else {
if (linesA[i] !== undefined) out.push({ type: 'removed', line: linesA[i] });
if (linesB[i] !== undefined) out.push({ type: 'added', line: linesB[i] });
}
}
return out;
}
lineDiff(
"hello\nworld\nfoo",
"hello\nWORLD\nfoo\nbar"
);
// [
// { type: 'unchanged', line: 'hello' },
// { type: 'removed', line: 'world' },
// { type: 'added', line: 'WORLD' },
// { type: 'unchanged', line: 'foo' },
// { type: 'added', line: 'bar' }
// ]
This naive walk produces wrong output when lines are inserted or deleted in the middle — every subsequent line shifts and looks like a change. Fine for "did anything change at all" checks, wrong for displaying meaningful diffs. For anything more, reach for a library.
Approach 2: jsdiff (Recommended for Production)
jsdiff is the de-facto standard JavaScript diff library. ~10KB minified, no dependencies, works in browser and Node.
// npm install diff
import * as Diff from 'diff';
const oldText = "hello\nworld\nfoo";
const newText = "hello\nWORLD\nfoo\nbar";
// Line-level diff
const changes = Diff.diffLines(oldText, newText);
changes.forEach(part => {
const prefix = part.added ? '+ ' : part.removed ? '- ' : ' ';
process.stdout.write(prefix + part.value);
});
// Word-level diff (better for prose)
const wordChanges = Diff.diffWords("hello world", "hello brave new world");
// [{ value: 'hello ', }, { added: true, value: 'brave new ' }, { value: 'world' }]
// Character-level diff (slowest, most precise)
const charChanges = Diff.diffChars("color", "colour");
// [{ value: 'colo' }, { added: true, value: 'u' }, { value: 'r' }]
// Unified diff (the format git diff produces)
const patch = Diff.createPatch("filename.txt", oldText, newText);
console.log(patch);
// Index: filename.txt
// ===================================================================
// --- filename.txt
// +++ filename.txt
// @@ -1,3 +1,4 @@
// hello
// -world
// +WORLD
// foo
// +bar
The library has matching diffSentences, diffCss, diffJson (semantic JSON diff), and diffArrays functions for specific use cases. applyPatch(text, patch) applies a unified diff back to the original text — useful for distributed editing.
Approach 3: diff-match-patch (Character-Level Precision)
Google's diff-match-patch is optimized for finding the minimum number of edits between two short strings. Best for spell-check, autocomplete suggestions, or real-time collaborative editors. It's slower than jsdiff for line-oriented diffs.
// npm install diff-match-patch
import DiffMatchPatch from 'diff-match-patch';
const dmp = new DiffMatchPatch();
const diffs = dmp.diff_main("The quick brown fox", "The slow brown dog");
dmp.diff_cleanupSemantic(diffs); // collapse trivial edits
// [[0, 'The '], [-1, 'quick'], [1, 'slow'], [0, ' brown '], [-1, 'fox'], [1, 'dog']]
// 0 = unchanged, -1 = removed, 1 = added
// Render as HTML
const html = dmp.diff_prettyHtml(diffs);
// "<span>The </span><del>quick</del><ins>slow</ins>..."
diff-match-patch also includes patch generation and fuzzy patch application — apply a patch even when the target text has drifted slightly from the patch's original context. Used inside Google Docs for real-time edit reconciliation.
Common Edge Cases
Whitespace and line endings
Windows uses \r\n, Unix uses \n. A file edited on both systems may diff against itself as 100% changed if you don't normalize. Pre-process:
const normalize = s => s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const diffs = Diff.diffLines(normalize(oldText), normalize(newText));
jsdiff also accepts {ignoreWhitespace: true} to ignore whitespace differences within lines and {newlineIsToken: true} for explicit newline handling.
Encoding differences (UTF-8 BOM, smart quotes)
A file with a leading UTF-8 byte-order mark () will diff against the same file without one. Strip with s.replace(/^/, ''). Curly quotes (" vs "), em-dashes vs hyphens, and similar Unicode lookalikes are real characters from the diff's perspective — pre-normalize if needed.
Trailing newlines
Some editors add a trailing newline; some don't. jsdiff reports this as an added or removed empty line. To ignore, normalize: s.replace(/\n+$/, '\n').
Diffing structured data
For JSON, YAML, or other structured data, line-level text diff often produces misleading results (a reformatted-but-equivalent file looks 100% changed). Use Diff.diffJson(a, b) for semantic JSON comparison, or parse both inputs into objects and use a deep-equality library like deep-equal or fast-deep-equal for "are these the same?" checks.
Performance Notes
jsdiff is fast for typical file sizes (under a few MB). Three things to watch for very large inputs:
- Line-level diffs scale linearly in the size of the input — 10MB completes in well under a second on modern hardware.
- Character-level diffs are quadratic in the worst case (Myers algorithm). For multi-MB inputs, character diffs can take minutes. Always start with line-level and only drill into characters where lines differ.
- UI blocks during compute — diff computation is synchronous. For anything over a few hundred KB, do the work in a Web Worker so the main thread stays responsive.
// Web Worker example
// worker.js
import * as Diff from 'diff';
self.onmessage = e => {
const result = Diff.diffLines(e.data.a, e.data.b);
self.postMessage(result);
};
// main.js
const worker = new Worker('worker.js', { type: 'module' });
worker.onmessage = e => renderDiff(e.data);
worker.postMessage({ a: oldText, b: newText });
Try It Live
The diff checker tool runs entirely in your browser using a line-level diff similar to what jsdiff produces — paste two pieces of text to see the differences highlighted instantly. For the Python equivalent of every pattern here, see the Python diff guide.
Frequently Asked Questions
What is the easiest way to compare two strings in JavaScript?
For equality alone, use the strict equality operator: a === b returns true if both strings are identical character by character. For locale-aware comparison (case folding, accented characters), use a.localeCompare(b) which returns -1, 0, or 1. For comparing two pieces of text and getting a list of what differs — added, removed, unchanged lines — you need a diff library. JavaScript has no built-in diff function. The most popular libraries are jsdiff and diff-match-patch, both small (a few KB) and easy to drop in.
What is the difference between jsdiff and diff-match-patch?
jsdiff (npm package diff) does line, word, and character diffs with multiple output formats including unified diff (the format git diff produces). API is simple: Diff.diffLines(oldText, newText) returns an array of change objects. Best for displaying diffs in a UI or generating patches. diff-match-patch (Google's library) does character-level diffs optimized for finding the minimum edit distance between two short strings — better for spell-check or real-time collaborative editing. It also generates and applies patches. For most "compare two files" tasks, jsdiff is the right pick.
How do I generate a unified diff in JavaScript?
Use jsdiff's createPatch() or createTwoFilesPatch(): const patch = Diff.createPatch("filename", oldText, newText) returns a string in unified diff format — the same format git diff, diff -u, and patch files use. You can apply the patch elsewhere with Diff.applyPatch(originalText, patchString). The unified format is human-readable, parseable by every diff tool, and the standard for sharing changes.
How do I ignore whitespace or case differences when diffing?
jsdiff's diff functions accept an options object with ignoreWhitespace and ignoreCase flags: Diff.diffLines(a, b, {ignoreWhitespace: true}). For finer control, normalize the strings first — collapse runs of whitespace with str.replace(/\s+/g, " ").trim(), lowercase with str.toLowerCase(), or strip trailing newlines with str.replace(/\r?\n$/, ""). Pre-normalizing is often clearer than configuring the library, especially when multiple normalization rules combine.
How fast can I diff a multi-megabyte file in the browser?
jsdiff is linear in the size of the diff and quadratic worst-case in the size of the input. For files up to a few hundred KB, diffing is essentially instant. For multi-megabyte inputs, line-level diffs (which work at the line granularity) stay fast — under a second for 10MB on a typical machine — but character-level diffs slow dramatically. For very large inputs, pre-process to extract just the changed regions (use file modification timestamps, content hashes, or chunked comparison) rather than diffing the whole thing. Run heavy diffs in a Web Worker so they do not block the UI thread.