A diff tool takes two versions of a text and reports what changed. That sounds like it needs to understand the content. It does not, it is solving a pure sequence problem, and every quirk in diff output follows from that.
The underlying problem
Given two sequences, find the longest common subsequence: the longest ordered set of elements appearing in both, not necessarily contiguously. Everything in the first sequence not in the LCS is a deletion; everything in the second not in the LCS is an insertion.
For:
A: the quick brown fox
B: the slow brown fox
The LCS is the … brown fox. So quick was deleted and slow was inserted. The tool has no concept of "the word was replaced", replacement is just a deletion adjacent to an insertion.
The Myers algorithm
Computing an LCS naively is O(N×M), which is unworkable on large files. Eugene Myers' 1986 algorithm reframes it as finding the shortest path through an edit graph and runs in O(ND), where D is the size of the difference.
The practical consequence is important: diff is fast when files are similar and slow when they are not. Comparing two versions of a file with three changed lines is nearly instantaneous. Comparing two unrelated files of the same size is much slower, because D is enormous.
This is why diffing minified JavaScript, one long line, everything different, can hang a tool that handles a 10,000-line source file instantly.
Granularity changes everything
The same algorithm produces very different output depending on what counts as an "element".
Line-based is what Git uses. Fast, and the right unit for code, where a line is a meaningful statement. Its weakness is prose: change one word in a paragraph and the whole paragraph shows as deleted and re-added.
Word-based is right for prose and documents, showing the actual words that changed within a sentence.
Character-based gives the finest detail and produces noisy, hard-to-read output on anything longer than a short string.
Many tools do both: line-level to find the changed regions, then word-level within those regions to highlight precisely what moved. That hybrid is what makes a good side-by-side view readable.
Why moved blocks look wrong
Move a function from the top of a file to the bottom. A standard diff shows a large deletion at the top and a large insertion at the bottom, with no indication that they are the same code.
That is correct output for the algorithm, an LCS is by definition order-preserving, so a reordering cannot be expressed as anything else. Some tools add move detection as a separate post-processing pass, comparing deleted and inserted blocks for similarity. It is a heuristic layered on top, not part of the core algorithm.
Similarly, reindenting a whole file produces a diff where every line changed. Most tools offer an "ignore whitespace" option that normalises before comparing, the first thing to reach for when a diff looks absurdly large.
Making diffs more useful
- Ignore whitespace when the change is a reformat, not a rewrite.
- Choose the granularity that matches the content. Line diff for code, word diff for a contract.
- Normalise line endings first. A file saved on Windows (CRLF) compared against the same file saved on Linux (LF) shows every single line as changed. This is a top-three cause of nonsense diffs.
- Diff the source, not the output. Comparing two PDFs or two Word documents means comparing rendered artefacts; comparing the text they contain is far more informative.
- Split unrelated changes. A diff containing a rename, a refactor and a bug fix is unreadable. This is a discipline point, not a tooling one.
Comparing documents rather than code
Diffing is also how you check whether a contract came back modified, or whether a supplier changed a clause in a specification. For those, text comparison and document comparison are the relevant tools, and since the documents in question are often confidential, running the comparison locally rather than pasting both versions into a web service is the sensible default.