Benchmarks

committed run: node v24.18.0 · AMD Ryzen 9 5900X 12-Core Processor · commit 9780e0e · jsdiff 5.2.2 · scripts/bench-vs-js.results.txt

Every number and chart on this page is generated from committed benchmark output using the same WebAssembly engine module as the site. That includes the cases where this engine loses; they are labeled exactly like the wins. The primary baseline is jsdiff (npm diff), the standard Myers-based diff library, so the comparison is against a real competitor and not a straw man; the repository's own JS reference (refdiff.mjs) is kept alongside it as an in-repo pipeline comparison. A checked-in script regenerates the page from the artifacts, and CI verifies that the displayed values match them.

The short version: against jsdiff, this engine is roughly at parity on typical diffs and dramatically faster on the pathological ones — the diffs that make a browser tab hang. That gap is not a micro-optimization; it is an algorithmic property, and it is the point of this page.

How it scales

The size-scaling family uses the same line-based content shape at growing sizes, with edits concentrated in a bounded zone. The chart plots three pipelines per case: the wasm engine, jsdiff, and the in-repo refdiff reference. The story is the two Myers implementations — jsdiff and the engine — tracking together: setting aside tiny-snippet (0.64x, where the wasm boundary floor costs more than the diff itself), the wasm pipeline stays within 1.01x to 1.43x of jsdiff across four orders of magnitude of input size. This is parity, and it is expected: both are Myers, so both scale the same way, and the engine's edge is the constant factor of running in wasm behind a compact boundary rather than a better asymptotic curve.

refdiff, a naive LCS, is the slower line over the mid range; the wasm pipeline overtakes it between tiny-snippet (360 chars, 0.73x) and small-10kb (10,389 chars, 1.58x). That crossover is a refdiff artifact, not the headline — the headline is parity with a real competitor, and the pathological cases where that parity breaks in the engine's favor are in the next section.

Median diff time by input size: refdiff, jsdiff, and the wasm pipeline Line chart with logarithmic axes over the 8 size-scaling cases, from tiny-snippet (360 characters) to huge-13mb-sparse (12,901,554 characters). The jsdiff and wasm pipeline curves track close together across the whole range because both are Myers; the in-repo refdiff reference runs higher over the mid range and the wasm pipeline overtakes it between 360 and 10,389 characters. Exact values are in the table below the chart. 0.1 1 10 100 1k 10k 100k 1M 10M crossover input size, characters per side (log scale) median time, ms (log scale) tiny-snippet: refdiff 0.049 ms small-10kb: refdiff 0.236 ms mid-40kb: refdiff 3.30 ms large-150kb-sparse: refdiff 5.89 ms lines-10k: refdiff 5.73 ms large-1mb-sparse: refdiff 10.26 ms huge-5mb-sparse: refdiff 80.58 ms huge-13mb-sparse: refdiff 200.51 ms tiny-snippet: jsdiff 0.043 ms small-10kb: jsdiff 0.158 ms mid-40kb: jsdiff 0.500 ms large-150kb-sparse: jsdiff 1.85 ms lines-10k: jsdiff 4.02 ms large-1mb-sparse: jsdiff 13.09 ms huge-5mb-sparse: jsdiff 72.53 ms huge-13mb-sparse: jsdiff 247.93 ms tiny-snippet: wasm pipeline 0.067 ms small-10kb: wasm pipeline 0.149 ms mid-40kb: wasm pipeline 0.451 ms large-150kb-sparse: wasm pipeline 1.52 ms lines-10k: wasm pipeline 3.05 ms large-1mb-sparse: wasm pipeline 12.94 ms huge-5mb-sparse: wasm pipeline 62.97 ms huge-13mb-sparse: wasm pipeline 173.46 ms wasm pipeline (compute + view assembly) jsdiff (npm diff, Myers) refdiff.mjs (in-repo reference)
Size-scaling cases: medians per pipeline (data for the chart above)
casewhat it isinput, charsrunsrefdiffjsdiffwasm (call + assembly)wasm (M10 page path)ratio vs jsdiff
tiny-snippetthe "Load example" pair every visitor sees (343 + 360 chars)343 + 360500.049 ms0.043 ms0.067 ms0.055 ms0.64x
small-10kb330 lines / ~10 KB, 12 edited lines in one zone10,350 + 10,389500.236 ms0.158 ms0.149 ms0.122 ms1.06x
mid-40kb1,300 lines / ~40 KB, 22 edited lines in one zone40,982 + 41,070503.30 ms0.500 ms0.451 ms0.367 ms1.11x
large-150kb-sparse5,000 lines / 150 KB, 44 edited lines (the committed marketing fixture)152,579 + 152,846505.89 ms1.85 ms1.52 ms1.23 ms1.21x
lines-10k10,000 lines / ~310 KB, 60 edited lines in one zone315,272 + 315,633505.73 ms4.02 ms3.05 ms2.48 ms1.32x
large-1mb-sparse33,000 lines / ~1 MB, 72 edited lines in one zone1,038,890 + 1,039,3962510.26 ms13.09 ms12.94 ms7.97 ms1.01x
huge-5mb-sparse165,000 lines / ~5.4 MB, 89 edited lines in one zone (the M10 large-file target)5,263,685 + 5,264,324780.58 ms72.53 ms62.97 ms39.36 ms1.15x
huge-13mb-sparse400,000 lines / ~13 MB, 97 edited lines in one zone12,900,754 + 12,901,5545200.51 ms247.93 ms173.46 ms113.38 ms1.43x

The M10 page-path column models compute plus lazy row-model construction and one 60-row render window. It excludes the worker round trip used by the current site for larger inputs, so it is a pipeline breakdown rather than a live per-keystroke measurement. The comparison ratio is jsdiff total over the wasm full-assembly total, where both materialize every row.

The bounded worst case

Parity holds until the diff gets hard. jsdiff, like every textbook Myers implementation, runs in O(ND) time, where D is the number of edits between the two sides: cheap when the files are close, but the cost climbs with the edit distance and runs away when the files are far apart. This engine caps its search depth at a fixed bound (MAX_D = 2048 line edits after trimming, crates/diffwtf-core/src/myers.rs); past the bound it degrades deterministically to a delete-all/insert-all diff instead of searching further. That cap was added as a safety valve against pathological memory use, and it turns out to be the entire performance story: the engine's worst case is bounded, and jsdiff's is not.

Two cases in the run cross that line. On complete-rewrite — 3,000 vs 3,000 lines with nothing in common: both engines take their documented degradation path (reference LCS bailout, engine depth cap) and emit the same del-all/ins-all diff — jsdiff spends 1152.93 ms where the engine spends 21.75 ms, a 53.01x difference, and the two produce the identical output: both delete every left line then insert every right line. Same answer, 53.01x less time. On adversarial-repeats — 700 lines per side drawn from a 12-line pool, independently shuffled: maximal ambiguity; both engines produce minimal diffs that may differ in shape but must agree on counts — the gap is 8.82x (28.58 ms against 3.24 ms); here the two diffs are both minimal (they agree on the +398/-398 line counts) but pick a different equally-minimal shape, which is expected on maximally ambiguous input and is disclosed rather than papered over.

The pathological tail: where jsdiff runs away and the engine does not
caseinput, charsjsdiffwasm (call + assembly)ratio vs jsdiffoutput vs jsdiff
complete-rewrite94,757 + 94,3651152.93 ms21.75 ms53.01xidentical diff (delete-all then insert-all)
adversarial-repeats21,607 + 21,55328.58 ms3.24 ms8.82xequally minimal, different shape

These are the diffs that make a browser tab hang: 1152.93 ms for a 3,000-line rewrite is well past the threshold where a keystroke feels broken. Nobody notices a 3 ms diff; everybody notices a one-second one. A bounded worst case is worth more here than a faster typical case, and it is why the two rows above — not the parity band above them — are the reason to compile the engine to wasm.

Where it loses

The same run includes the cases where the wasm pipeline is slower than jsdiff or not directly comparable, measured with the same methodology as everything else:

Every case

Every case in the run, ranked by the jsdiff bar. The cluster around 1x is the parity band; the two bars that run off to the right are the bounded-worst-case wins. The vs refdiff column is the in-repo comparison for context — where it and vs jsdiff disagree, both are shown rather than the flattering one alone.

Speed ratio per benchmark case, jsdiff time divided by wasm time Bar chart of all 13 cases on a logarithmic scale with the 1x parity line marked. Most cases cluster near 1x (parity with jsdiff); two pathological cases extend far to the right where the engine is many times faster. Bars left of 1x are cases where the wasm pipeline is slower than jsdiff. Exact values are in the table below the chart. 0.5x 1x 2x 5x 10x 20x 50x 1x = same speed as jsdiff tiny-snippet 0.64x (slower than jsdiff) small-10kb 1.06x mid-40kb 1.11x large-150kb-sparse 1.21x large-150kb-identical 1.16x (fast path, not engine speed) lines-10k 1.32x large-1mb-sparse 1.01x huge-5mb-sparse 1.15x huge-13mb-sparse 1.43x large-1mb-spread 1.41x complete-rewrite 53.01x adversarial-repeats 8.82x minified-json 2.49x jsdiff time / wasm time (log scale; right of 1x, the engine is faster)
All cases: medians per pipeline (data for the chart above)
casewhat it isinput, charsrunsrefdiffjsdiffwasm (call + assembly)vs jsdiffvs refdiff
tiny-snippetthe "Load example" pair every visitor sees (343 + 360 chars)343 + 360500.049 ms0.043 ms0.067 ms0.64x0.73x
small-10kb330 lines / ~10 KB, 12 edited lines in one zone10,350 + 10,389500.236 ms0.158 ms0.149 ms1.06x1.58x
mid-40kb1,300 lines / ~40 KB, 22 edited lines in one zone40,982 + 41,070503.30 ms0.500 ms0.451 ms1.11x7.32x
large-150kb-sparse5,000 lines / 150 KB, 44 edited lines (the committed marketing fixture)152,579 + 152,846505.89 ms1.85 ms1.52 ms1.21x3.87x
large-150kb-identical150 KB, byte-identical sides: measures the identical-input fast path plus boundary floor, NOT engine speed152,579 + 152,579500.688 ms1.30 ms1.12 ms1.16x0.62x
lines-10k10,000 lines / ~310 KB, 60 edited lines in one zone315,272 + 315,633505.73 ms4.02 ms3.05 ms1.32x1.88x
large-1mb-sparse33,000 lines / ~1 MB, 72 edited lines in one zone1,038,890 + 1,039,3962510.26 ms13.09 ms12.94 ms1.01x0.79x
huge-5mb-sparse165,000 lines / ~5.4 MB, 89 edited lines in one zone (the M10 large-file target)5,263,685 + 5,264,324780.58 ms72.53 ms62.97 ms1.15x1.28x
huge-13mb-sparse400,000 lines / ~13 MB, 97 edited lines in one zone12,900,754 + 12,901,5545200.51 ms247.93 ms173.46 ms1.43x1.16x
large-1mb-spread33,000 lines / ~1 MB, ~110 edits spread across the whole file: past the reference LCS bailout, so the reference degrades to a full rewrite while the engine stays minimal; output quality differs and added/removed counts are reported per side1,040,315 + 1,040,9312567.47 ms14.94 ms10.57 ms1.41x6.38x
complete-rewrite3,000 vs 3,000 lines with nothing in common: both engines take their documented degradation path (reference LCS bailout, engine depth cap) and emit the same del-all/ins-all diff94,757 + 94,365256.81 ms1152.93 ms21.75 ms53.01x0.31x
adversarial-repeats700 lines per side drawn from a 12-line pool, independently shuffled: maximal ambiguity; both engines produce minimal diffs that may differ in shape but must agree on counts21,607 + 21,553506.34 ms28.58 ms3.24 ms8.82x1.96x
minified-jsonsingle-line minified JSON, ~150 KB per side, one value edited mid-string: exercises intra-line refinement with prefix/suffix trimming on one huge line145,124 + 145,145508.50 ms8.98 ms3.61 ms2.49x2.36x

Counts note: jsdiff (real Myers) agrees with the engine on added/removed line counts on every case, which the benchmark asserts before timing. refdiff does not: on large-1mb-spread it degrades past its LCS bailout (js +32701/-32701 vs wasm +110/-110 (documented divergence: reference degrades past its bailout)), so its ratio there compares different amounts of useful work and its vs refdiff number favors the engine. The large-150kb-identical bar measures the disclosed identical-input fast path plus the boundary floor, not engine speed. minified-json compares against jsdiff diffWords, not diffLines: its single line makes the line-level diff a non-comparison against the engine's intra-line refinement.

Where the time goes

Phases of the wasm pipeline on large-150kb-sparse: 5,000 lines / 150 KB, 44 edited lines (the committed marketing fixture). The result marshal is the cost of crossing the wasm boundary, the part M9 rewrote: with the sparse contract it is 0.034 ms here, measured as the difference between the compute call and a probe call that does the same work but returns only a checksum.

Where the time goes on the 150 KB fixture Bar chart of the wasm pipeline phases for the large-150kb-sparse case on a linear scale from a zero baseline, with the JS reference total as context. The boundary marshal is 0.034 ms. Exact values are in the table below the chart. 0 1 2 3 4 5 refdiff total, for context 5.89 ms jsdiff total, for context 1.85 ms wasm engine compute (probe) 1.01 ms result marshal across the boundary 0.034 ms full view assembly (pre-M10 page path) 0.478 ms row model build (M10 page path) 0.177 ms one 60-row render window (M10) 0.003 ms median time, ms (linear, zero baseline)
Phase medians on large-150kb-sparse (data for the chart above)
phasemedian
refdiff total (compute incl. views), for context5.89 ms
jsdiff total (incl. views), for context1.85 ms
wasm engine compute (probe)1.01 ms
wasm compute call (engine + boundary)1.05 ms
result marshal across the boundary (derived)0.034 ms
full view assembly in JS (pre-M10 page path)0.478 ms
row model build (M10 page path)0.177 ms
one 60-row render window (M10)0.003 ms
wasm total (call + full assembly)1.52 ms
wasm total (M10 page path)1.23 ms

In a real browser tab

The Node numbers above compare compute. A browser tab also has to render the result, and painting every row of a large diff into the DOM dwarfs either compute path. Measured in headless Chromium on the real page (node v24.18.0 · AMD Ryzen 9 5900X 12-Core Processor · commit c00db09, scripts/bench-browser.results.txt): on the 150 KB fixture the shared split-view DOM render is 239.00 ms, so despite a 4.25x compute win the end-to-end ratio including render is 1.02x. Stated plainly: once the full DOM render dominates, the engines tie. That is why M10 made the site render a virtualized window instead of every row.

The browser run predates the jsdiff baseline and still compares against refdiff, so its compute ratios are the refdiff numbers, not the jsdiff ones; the point it exists to make — that a full DOM render swamps any compute difference — is independent of which baseline the compute is measured against. Against jsdiff, where compute is already near parity on these sparse cases, the end-to-end tie is only more immediate. Adding jsdiff to the browser harness is follow-up work; the Node run above is where the engine-versus-jsdiff comparison lives.

Compute plus DOM render on the 150 KB fixture in Chromium Two stacked bars on a linear scale from a zero baseline. Rendering the split view into the DOM takes 239.00 ms for both pipelines, so end to end the two finish within about 1.02x of each other despite the 4.25x compute gap. Exact values are in the table below. JS reference: compute 6.80 ms + DOM render 239.00 ms = 245.80 ms wasm pipeline: compute 1.60 ms + DOM render 239.00 ms = 240.60 ms ms, linear, zero baseline; the gray span is the shared DOM render of the split view
Headless Chromium, real page: medians per case
caseinput, charsrunsJS referencewasm (call + assembly)DOM render (shared)ratio w/o renderratio incl. render
tiny-snippet343 + 36030 (render: 30)0.100 ms0.100 ms1.20 ms1.00x1.00x
large-150kb-sparse152,579 + 152,84630 (render: 10)6.80 ms1.60 ms239.00 ms4.25x1.02x
large-150kb-identical152,579 + 152,57930 (render: 10)0.600 ms1.10 ms205.30 ms0.55x1.00x
lines-10k315,272 + 315,63320 (render: 5)5.95 ms2.40 ms429.80 ms2.48x1.01x
large-1mb-sparse1,038,890 + 1,039,39610 (render: 2)8.90 ms7.00 ms1754.20 ms1.27x1.00x

The large-150kb-identical row measures the disclosed identical-input fast path plus the boundary floor, not engine speed.

Engine size

The engine ships as a 45.7 KB WebAssembly binary (measured from the built web/pkg/diffwtf_wasm_bg.wasm) plus generated JS glue. The site uses static HTML, CSS, and vanilla-JS modules; diff computation requires no server round trip.

Methodology

Every displayed benchmark value is parsed out of the committed artifacts by scripts/gen-bench-page.mjs, which regenerates this page and the home page chart; CI fails if the pages and artifacts diverge. CI never runs the benchmark itself — it only checks the committed numbers against the committed pages — so no benchmark dependency is installed in CI.

Main run (scripts/bench-vs-js.results.txt): node v24.18.0 · AMD Ryzen 9 5900X 12-Core Processor · commit 9780e0e · jsdiff 5.2.2. Reported values are medians, 10 warmup runs per pipeline (huge cases override per bench-cases.mjs), word granularity, interleaved. Three pipelines run in the same Node process (V8, the engine Chrome uses), interleaved so none gets a systematic cache or GC advantage: the wasm engine module the site ships, jsdiff (npm diff, pinned; a dev-only dependency of the benchmark harness that is never bundled into the site), and the in-repo refdiff reference. jsdiff runs diffLines for line structure, except minified-json, whose single line makes diffLines a non-comparison, where it runs diffWords to match the engine's intra-line refinement. Inputs are committed fixtures or deterministic seeded generators (scripts/bench-cases.mjs), reproducible from any checkout.

Sanity checks run before timing: jsdiff and the engine must agree on added and removed line counts on every case (jsdiff is real Myers, so it agrees even where refdiff degrades past its LCS bailout on large-1mb-spread), and every pipeline's output must reconstruct both inputs. All three totals include materializing every row into a renderable view, so the comparison charges each pipeline for the same view work and none is credited for skipping it. On complete-rewrite the engine and jsdiff verifiably emit the identical diff; on the ambiguous adversarial-repeats they emit different but equally minimal diffs — both are checked, not assumed.

Fairness notes, disclosed: the engine additionally refines within changed lines (word-level highlights) on the line cases, which jsdiff diffLines does not, so on the sparse cases the engine reaches parity while doing more, not less. The identical-input fast path is a product shortcut and never backs an engine-speed claim. The Node runner is not a browser page; the browser run exists to check that the story holds there.

Reproduce it: npm install (to fetch the pinned jsdiff), ./scripts/build-wasm.sh, then node scripts/bench-vs-js.mjs (and node scripts/bench-browser.mjs for the Chromium run), from the repo. The committed results files are regenerated by piping stdout there and committing the diff, so the artifact history is reviewable like any other code.