Skip to Content
New: @blazediff/core-native now includes interpret - structured diff analysis to understand what changed. Read more →
Documentation@blazediff/core-wasm

@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%.

View Detailed Benchmarks 

Installation

npm install @blazediff/core-wasm

Ships ~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

ParameterTypeDescription
aUint8ArrayFirst image in RGBA8 order (width * height * 4 bytes)
bUint8ArraySecond image in RGBA8 order (same length)
widthnumberImage width in pixels
heightnumberImage height in pixels
outputUint8Array | undefinedOptional diff visualization output (same length). Written in place
optionsDiffOptionsComparison options (optional)
Options
OptionTypeDefaultDescription
thresholdnumber0.1Color difference threshold (0.0-1.0). Lower = more strict
includeAAbooleanfalseCount anti-aliased pixels as differences
diffMaskbooleanfalseRender 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):

Fixturepixelmatchcore-wasmImprovement
4k/1287.72ms51.75ms82.0%
4k/3366.81ms69.90ms80.9%
page/2443.83ms109.74ms75.3%
blazediff/314.60ms5.52ms62.2%
pixelmatch/10.87ms0.13ms84.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 casePackage
Browser, edge worker, wasm host@blazediff/core-wasm
Node CLI / server with native bin@blazediff/core-native
Pure JS / no wasm support@blazediff/core
Last updated on