@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). ~58% faster than pixelmatch on the same RGBA buffers; diff counts agree with pixelmatch to within ~0.05%.
Installation
npm install @blazediff/core-wasmShips ~32 KB of optimized wasm + ~10 KB of JS glue. No native binaries, no postinstall, no platform packages.
Features
- Same algorithm as
@blazediff/core-native: YIQ perceptual delta + block-based cold/hot pass - wasm32 v128 SIMD (
+simd128): 4-lane vectorized cold and hot loops; up to ~16× faster than pixelmatch on 4K - Buffers-only API: caller decodes images, hands in
Uint8Array. No PNG/JPEG codecs bundled - Runs anywhere wasm runs: browsers (
fetch()of the .wasm), Node (fs.readFileSync+ bytes), Cloudflare Workers, Deno, Bun
API Reference
initBlazediff(input?)
Initializes the wasm module. Safe to call multiple times; subsequent calls return the cached promise. The function 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, which works in browsers but fails in runtimes whose fetch() cannot resolve the resulting URL (Node file://, Workers, etc.).
Loading the wasm module
Pick the recipe that matches your runtime. All four are equivalent; the wasm itself is the same.
Browser, plain script tag or ESM. The default works because the sibling .wasm is reachable via fetch(import.meta.url):
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/core-wasm@4.2.0/wasm/blazediff_bg.wasm',
),
);Pin the version (@4.2.0) for reproducibility. unpkg.com/@blazediff/core-wasm@4.2.0/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. Path resolution depends on your module system:
// 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));diff(a, b, width, height, output?, options?)
Compares two RGBA pixel buffers and returns the number of differing pixels.
Parameters
| Parameter | Type | Description |
|---|---|---|
a | Uint8Array | First image in RGBA8 order (width * height * 4 bytes) |
b | Uint8Array | Second image in RGBA8 order (same length) |
width | number | Image width in pixels |
height | number | Image height in pixels |
output | Uint8Array | undefined | Optional diff visualization output (same length). Written in place |
options | DiffOptions | Comparison options (optional) |
Options
| Option | Type | Default | Description |
|---|---|---|---|
threshold | number | 0.1 | Color difference threshold (0.0-1.0). Lower = more strict |
includeAA | boolean | false | Count anti-aliased pixels as differences |
diffMask | boolean | false | Render diff with transparent background instead of grayscale base |
Threshold Guidelines: 0.0 exact match · 0.05 strict · 0.1 default · 0.2 lenient
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);
const { data } = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
return { data: new Uint8Array(data.buffer), width: bitmap.width, height: bitmap.height };
}
const a = await toRgba('/baseline.png');
const b = await toRgba('/current.png');
const out = new Uint8Array(a.width * a.height * 4);
const diffCount = await diff(a.data, b.data, a.width, a.height, out, {
threshold: 0.1,
});
console.log(`${diffCount} pixels differ`);Node
The wasm can be loaded either from jsDelivr (zero-config, works in Node 18+) or from the local node_modules install. The CDN form is shown here; see Loading the wasm module for the offline readFileSync recipe.
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/core-wasm@4.2.0/wasm/blazediff_bg.wasm',
),
);
const a = PNG.sync.read(readFileSync('baseline.png'));
const b = PNG.sync.read(readFileSync('current.png'));
const diffCount = await diff(
new Uint8Array(a.data),
new Uint8Array(b.data),
a.width,
a.height,
);Performance
vs pixelmatch on M1 Max, image I/O excluded (pre-decoded RGBA buffers):
| Fixture | pixelmatch | core-wasm | Improvement |
|---|---|---|---|
| 4k/1 | 287.72ms | 51.75ms | 82.0% |
| 4k/3 | 366.81ms | 69.90ms | 80.9% |
| page/2 | 443.83ms | 109.74ms | 75.3% |
| blazediff/3 | 14.60ms | 5.52ms | 62.2% |
| pixelmatch/1 | 0.87ms | 0.13ms | 84.6% |
Average ~58% faster across the full fixture set. Counts agree with pixelmatch within ~0.05% (e.g. 4k/1: 69 932 vs 69 912 of 17 920 000 pixels; both use YIQ perceptual delta, residual differences are anti-aliasing edge cases).
Why so fast? Block-based two-pass algorithm (cold pass skips unchanged 8×8 blocks via integer compare, hot pass runs YIQ delta only on changed regions) with v128 SIMD intrinsics: 4-lane vectorized RGBA extraction, YIQ transform, and threshold compare.
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 |