BlazeDiff vs odiff
@blazediff/core-native is ~2.5x faster
than odiff over our 34 fixtures, with image decode and diff PNG writing
included. Both are native binaries with a Node API, and both return the same
kind of result, so most code moves over with a changed import.
odiff is a SIMD image diff written in Zig, popular in visual testing tools. It decodes many image formats and has a server mode that keeps one process alive between comparisons. BlazeDiff runs a block-based Rust kernel with SIMD on ARM and x86, and it reads file paths or encoded bytes.
At a glance
| odiff | BlazeDiff (core-native) | |
|---|---|---|
| License | MIT | MIT |
| Implementation | Zig, SIMD (SSE2, AVX2, AVX512, NEON) | Rust, SIMD (NEON, SSE4.1) |
| Delivery | Native binary, Node API, CLI | N-API addon, @blazediff/cli, Python, Rust |
| Input | File paths, buffers | File paths, encoded bytes |
| Formats | PNG, JPEG, WebP, TIFF, BMP | PNG, JPEG, QOI |
| Color metric | YIQ | YIQ |
| Ignore regions | Yes (rectangles) | No (selector masks in @blazediff/agent) |
| Browser / edge | No | Yes, through @blazediff/core-wasm |
| Beyond pixel counts | Changed lines and columns | Region analysis, SSIM, GMSD and MILO packages |
Benchmarks
odiff runs through ODiffServer, its server mode, so process start-up is not
counted against it. βBlazeDiff Nextβ is the same binary with the
blazediff-png codec turned on through
BLAZEDIFF_PNG_ENABLED=1.
JavaScript Native Binary (@blazediff/core-native vs odiff) (image IO included)
TOTAL TIME Β· 34 FIXTURES Β· MEAN OF 25 RUNS Β· IO INCLUDED
Apple M1 Max Β· darwin/arm64 Β· Node v22.14.0 Β· rustc 1.97.1 (8bab26f4f 2026-07-14) Β· 2026-10-22 Β· 234b300-dirty Β· pnpm benchmark core-native
Per-fixture results (34)
| Fixture | ODiff | BlazeDiff | BlazeDiff Next | BlazeDiff vs ODiff | BlazeDiff Next vs ODiff |
|---|---|---|---|---|---|
| 4k/1 | 1.30s | 316ms | 233ms | 4.09x | 5.55x |
| 4k/1 (identical) | 271ms | 187ms | 150ms | 1.45x | 1.81x |
| 4k/2 | 1.56s | 393ms | 313ms | 3.97x | 4.99x |
| 4k/2 (identical) | 338ms | 224ms | 167ms | 1.50x | 2.02x |
| 4k/3 | 1.89s | 393ms | 300ms | 4.80x | 6.30x |
| 4k/3 (identical) | 389ms | 231ms | 175ms | 1.69x | 2.23x |
| blazediff/1 | 3.92ms | 3.08ms | 2.97ms | 1.27x | 1.32x |
| blazediff/1 (identical) | 1.53ms | 0.87ms | 0.85ms | 1.75x | 1.81x |
| blazediff/2 | 4.29ms | 3.57ms | 2.06ms | 1.20x | 2.08x |
| blazediff/2 (identical) | 1.39ms | 1.06ms | 0.76ms | 1.31x | 1.83x |
| blazediff/3 | 48.95ms | 32.15ms | 20.54ms | 1.52x | 2.38x |
| blazediff/3 (identical) | 18.79ms | 14.28ms | 11.42ms | 1.32x | 1.64x |
| blazediff/4 | 23.79ms | 26.31ms | 23.92ms | 0.90x | 0.99x |
| blazediff/4 (identical) | 10.93ms | 4.61ms | 3.80ms | 2.37x | 2.87x |
| page/1 | 998ms | 675ms | 548ms | 1.48x | 1.82x |
| page/1 (identical) | 542ms | 249ms | 166ms | 2.18x | 3.27x |
| page/2 | 607ms | 415ms | 280ms | 1.46x | 2.17x |
| page/2 (identical) | 124ms | 42.18ms | 32.85ms | 2.93x | 3.76x |
| pixelmatch/1 | 3.84ms | 2.23ms | 1.21ms | 1.72x | 3.16x |
| pixelmatch/1 (identical) | 1.73ms | 1.05ms | 0.84ms | 1.64x | 2.05x |
| pixelmatch/2 | 3.54ms | 1.79ms | 1.72ms | 1.98x | 2.06x |
| pixelmatch/2 (identical) | 0.53ms | 0.47ms | 0.22ms | 1.12x | 2.40x |
| pixelmatch/3 | 3.25ms | 1.42ms | 1.30ms | 2.28x | 2.49x |
| pixelmatch/3 (identical) | 1.67ms | 0.87ms | 0.63ms | 1.91x | 2.65x |
| pixelmatch/4 | 9.61ms | 4.85ms | 4.72ms | 1.98x | 2.04x |
| pixelmatch/4 (identical) | 2.64ms | 2.16ms | 1.25ms | 1.22x | 2.11x |
| pixelmatch/5 | 0.79ms | 0.97ms | 0.70ms | 0.82x | 1.14x |
| pixelmatch/5 (identical) | 0.85ms | 0.56ms | 0.41ms | 1.52x | 2.08x |
| pixelmatch/6 | 8.90ms | 2.08ms | 2.39ms | 4.29x | 3.73x |
| pixelmatch/6 (identical) | 0.98ms | 0.83ms | 0.57ms | 1.18x | 1.71x |
| pixelmatch/7 | 3.60ms | 2.64ms | 1.80ms | 1.36x | 2.00x |
| pixelmatch/7 (identical) | 0.72ms | 0.63ms | 0.31ms | 1.15x | 2.36x |
| same/1 | 5.04ms | 3.09ms | 2.95ms | 1.63x | 1.71x |
| same/1 (identical) | 5.50ms | 3.20ms | 2.58ms | 1.72x | 2.13x |
| TOTAL | 8.18s | 3.24s | 2.45s | 2.52x | 3.33x |
Switching
The result is a discriminated union in both libraries: match, then a reason
of layout-diff, pixel-diff or file-not-exists, with diffCount and
diffPercentage on a pixel diff.
// Before
import { compare } from "odiff-bin";
// After
import { compare } from "@blazediff/core-native";
const result = await compare("baseline.png", "current.png", "diff.png", {
threshold: 0.1,
antialiasing: true,
});
if (!result.match && result.reason === "pixel-diff") {
console.log(`${result.diffCount} pixels differ`);
}odiff options such as ignoreRegions, captureDiffLines and
reduceRamUsage have no direct counterpart. Remove them, or mask the
regions before the diff.
When to keep odiff
- You compare WebP, TIFF or BMP files, or a JPEG against a PNG.
- You depend on
ignoreRegionsor on the changed-line output. - You use a tool that already integrates odiff, for example Argos or Lost Pixel.