@blazediff/core-wasm
WebAssembly build of the BlazeDiff Rust algorithm for browsers, edge runtimes, and any wasm host. Same two-pass block algorithm as @blazediff/core-native, compiled to wasm32 with v128 SIMD (+simd128). ~51% faster than pixelmatch on the same RGBA buffers (up to ~90% on 4K); diff counts agree with pixelmatch to within ~0.05%.
Features:
- Same Rust algorithm as
@blazediff/core-native(YIQ perceptual delta + block-based cold/hot pass) wasm32v128 SIMD: 4-lane vectorized cold and hot loops; up to ~16x faster than pixelmatch on 4K- Same
Image,DiffOptionsandDiffResultas@blazediff/core. Caller decodes images; no PNG/JPEG codecs bundled - ~32 KB optimized wasm + ~10 KB JS glue. No native binaries, no postinstall, no platform packages
- Runs anywhere wasm runs: browsers, Node 18+, Cloudflare Workers, Deno, Bun
Installation
npm install @blazediff/core-wasm
Loading the wasm module
Pick the recipe that matches your runtime. All four are equivalent; the wasm itself is the same.
Browser (default fetch)
The wasm-bindgen --target web glue fetches the sibling .wasm via import.meta.url automatically:
import { initBlazediff } from '@blazediff/core-wasm';
await initBlazediff();
Universal CDN URL (recommended for Node, Workers, Deno, Bun)
jsDelivr serves the published .wasm over HTTPS, so any fetch()-capable runtime can load it. One network round-trip on cold start, cached by the runtime after that:
import { initBlazediff } from '@blazediff/core-wasm';
await initBlazediff(
new URL(
'https://cdn.jsdelivr.net/npm/@blazediff/[email protected]/wasm/blazediff_bg.wasm',
),
);
Pin the version (@7.0.0). It must match the installed package, because the JS glue and the .wasm change together. unpkg.com/@blazediff/[email protected]/wasm/blazediff_bg.wasm works identically.
Bundlers (Vite, Webpack 5+, esbuild, Rollup with plugin)
The new URL(asset, import.meta.url) pattern is bundler-aware: the asset is emitted into the build output and the URL is rewritten at build time:
import { initBlazediff } from '@blazediff/core-wasm';
const wasmUrl = new URL(
'@blazediff/core-wasm/wasm/blazediff_bg.wasm',
import.meta.url,
);
await initBlazediff(wasmUrl);
Node from the local filesystem
Offline, no CDN dependency. Read the bytes and pass them in:
// ESM (Node 20.6+):
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { initBlazediff } from '@blazediff/core-wasm';
const wasmPath = fileURLToPath(
import.meta.resolve('@blazediff/core-wasm/wasm/blazediff_bg.wasm'),
);
await initBlazediff(readFileSync(wasmPath));
// CommonJS:
const { readFileSync } = require('node:fs');
const { initBlazediff } = require('@blazediff/core-wasm');
const wasmPath = require.resolve(
'@blazediff/core-wasm/wasm/blazediff_bg.wasm',
);
await initBlazediff(readFileSync(wasmPath));
API
initBlazediff(input?)
Initializes the wasm module. Safe to call multiple times; subsequent calls return the cached promise. Accepts a URL, Response, ArrayBuffer, Uint8Array, or compiled WebAssembly.Module. Without input, the default --target web glue fetches the sibling blazediff_bg.wasm via import.meta.url (works in browsers; other runtimes need an explicit input, see above).
Returns: Promise<void>
diff(a, b, options?)
Compares two RGBA images. Takes and returns the same types as @blazediff/core's diff, which they are imported from.
| Parameter | Type | Description |
|---|---|---|
a, b |
Image | { data, width, height } with RGBA8 pixels (width * height * 4 bytes). Browser ImageData fits as is |
options |
DiffOptions | Comparison options (optional) |
Returns: Promise<DiffResult>
| Option | Type | Default | Description |
|---|---|---|---|
threshold |
number | 0.1 | Color difference threshold (0-1). Lower is stricter |
antialiasing |
boolean | true | Detect anti-aliased pixels and leave them out of the count |
metric |
"yiq" | "oklab" | "yiq" | Color difference metric. oklab gives the same counts as pixelmatch 8 |
mask |
Uint8Array | - | One byte per pixel (width × height). Non-zero pixels are skipped |
window |
number | - | Also report the N×N window with the most diff pixels |
output |
Image | - | Draw the diff into this image in place. Identical images leave it unwritten |
render |
{ alpha?, diffColor?, diffColorAlt?, aaColor?, background? } | - | Output styling. background: "transparent" draws the diff alone |
type DiffResult =
| {
kind: "compared";
count: number;
percentage: number;
identical: boolean;
/** Top-left corner and count of the busiest window, when `window` is set. */
window?: { x: number; y: number; count: number };
}
| { kind: "dimension-mismatch"; a: Size; b: Size };
Invalid options or buffers reject with a BlazeDiffError that has a code: invalid-image, invalid-output, invalid-mask or invalid-option.
const result = await diff(a, b, {
output: { data: out, width, height },
render: { diffColorAlt: [0, 128, 255] },
});
Region analysis — what changed rather than where — is no longer part of this
package. It lives in
@blazediff/interpret-native,
which is a Node N-API package and has no wasm build.
Usage
Browser
Decode images via createImageBitmap + OffscreenCanvas (or the ImageDecoder API), then pass the RGBA buffer to diff():
import { diff, initBlazediff } from '@blazediff/core-wasm';
await initBlazediff();
async function toRgba(url: string) {
const bitmap = await createImageBitmap(await (await fetch(url)).blob());
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d')!;
ctx.drawImage(bitmap, 0, 0);
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
}
const a = await toRgba('/baseline.png');
const b = await toRgba('/current.png');
const output = new ImageData(a.width, a.height);
const result = await diff(a, b, { output, threshold: 0.1 });
if (result.kind === 'compared') console.log(`${result.count} pixels differ`);
Node
import { readFileSync } from 'node:fs';
import { diff, initBlazediff } from '@blazediff/core-wasm';
import { PNG } from 'pngjs';
await initBlazediff(
new URL(
'https://cdn.jsdelivr.net/npm/@blazediff/[email protected]/wasm/blazediff_bg.wasm',
),
);
const a = PNG.sync.read(readFileSync('baseline.png'));
const b = PNG.sync.read(readFileSync('current.png'));
const result = await diff(a, b);
Performance
vs pixelmatch on M1 Max, image I/O excluded (pre-decoded RGBA buffers):
| Fixture | pixelmatch | core-wasm | Improvement |
|---|---|---|---|
| 4k/1 | 338.53ms | 34.42ms | 89.8% |
| 4k/3 | 415.01ms | 45.34ms | 89.1% |
| page/2 | 511.88ms | 70.89ms | 86.2% |
| blazediff/3 | 12.08ms | 5.76ms | 52.3% |
| pixelmatch/1 | 0.57ms | 0.11ms | 80.5% |
Average ~51% faster across the full fixture set (~60% across differing pairs). Counts agree with pixelmatch within ~0.05% (e.g. 4k/1: 69 932 vs 69 912 of 17 920 000 pixels).
Full benchmarks in BENCHMARKS.md.
Algorithm
Same two-pass block-based approach as @blazediff/core-native, recompiled for wasm:
- Cold pass: scans the image in 8x8 blocks using 32-bit integer comparison to identify changed regions
- Hot pass: only processes blocks marked as changed, applying YIQ perceptual color difference
- SIMD:
v128intrinsics (f32x4_*,i32x4_*) for parallel 4-lane RGBA extraction, alpha blend, YIQ transform, and threshold compare. Baseline simd128 has no native FMA, so weighted sums useadd(mul, c) - Anti-aliasing: Vysniauskas (2009) algorithm to detect AA artifacts
Picking the right package
| Use case | Package |
|---|---|
| Browser, edge worker, wasm host | @blazediff/core-wasm |
| Node CLI / server with native bin | @blazediff/core-native |
| Pure JS / no wasm support | @blazediff/core |
References
- YIQ Color Space: Kotsarenko & Ramos (2009)
- Anti-Aliasing Detection: Vysniauskas (2009)
- WebAssembly SIMD: WebAssembly/simd (v128 / simd128 specification)