Skip to content
Image KB Reducer
Esc
↑↓navigate↵open⌘Jpreview
On this page

Backends

canvasBackend, nodeBackend, and writing your own WasmModules for a host like Cloudflare Workers.

Every PixelBackend runs the same job model — runJob never knows or cares which one it was handed. Which backend you use is entirely a property of the realm your code runs in.

nodeBackend() — Node

import { nodeBackend } from "@imagekbreducer/image-core/node"

const backend = nodeBackend()

Pure wasm, no native module, no node-canvas. nodeBackend() is memoised per process — every job in a batch (the CLI compressing a directory of files, for example) shares one set of already- instantiated codecs, the same realm-wide sharing a browser tab’s own codec cache provides. Under the hood, it resolves each .wasm binary through Node’s own module resolver (so it finds the file wherever your package manager actually put it, not a path guessed from __dirname) and compiles it once.

This is the only entry point in the package that imports node:* at all — everything else, including the wasm backend itself, is host-agnostic on purpose. That’s what lets the same backend serve a host with no filesystem at all, below.

canvasBackend — browser

import { canvasBackend, getCapabilities } from "@imagekbreducer/image-core/browser"

const backend = canvasBackend()

Uses the platform’s own <canvas>/OffscreenCanvas for decode and JPEG encode, with wasm codecs for the formats canvas can’t produce reliably (WebP quality control, PNG’s oxipng ladder). This is what imagekbreducer.com itself runs — see Parity with the website for exactly how (and how much) its output differs from the wasm backend’s.

./browser also exports createWorkerClient/createInlineClient (hand a job to a Web Worker instead of running it on the main thread), supportsModuleWorker, and warmup (pre-load codecs before the first real job needs them).

Writing your own WasmModules — Cloudflare Workers, or any host with no filesystem

nodeBackend() and canvasBackend() are two different hosts wired to the same underlying wasm backend — everything under ./backend/wasm/** never touches node:*, a DOM global, or a filesystem. It asks its host for compiled codecs by name and nothing else, which is exactly what lets it run somewhere with no filesystem to read from and no fetch() for its own bundle — a Cloudflare Worker.

export interface WasmModules {
  load(name: WasmName): Promise<WebAssembly.Module>
}

WasmName names every wasm binary the backend can ask for — one per .wasm file, not per codec (mozjpeg ships separate encoder and decoder binaries):

type WasmName =
  | "mozjpeg_enc" | "mozjpeg_dec"
  | "squoosh_png_bg"
  | "webp_enc" | "webp_dec"
  | "squoosh_oxipng_bg"
  | "squoosh_resize_bg"

A Worker can import a .wasm file directly — the runtime resolves it to a WebAssembly.Module at deploy time, with no fetch() or filesystem call needed:

// worker/wasm-modules.ts
import mozjpegEnc from "@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm"
import mozjpegDec from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm"
import squooshPng from "@jsquash/png/codec/pkg/squoosh_png_bg.wasm"
import webpEnc from "@jsquash/webp/codec/enc/webp_enc.wasm"
import webpDec from "@jsquash/webp/codec/dec/webp_dec.wasm"
import oxipng from "@jsquash/oxipng/codec/pkg/squoosh_oxipng_bg.wasm"
import resize from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm"
import { createWasmBackend } from "@imagekbreducer/image-core/wasm"
import type { WasmModules, WasmName } from "@imagekbreducer/image-core/wasm"

const MODULES: Record<WasmName, WebAssembly.Module> = {
  mozjpeg_enc: mozjpegEnc,
  mozjpeg_dec: mozjpegDec,
  squoosh_png_bg: squooshPng,
  webp_enc: webpEnc,
  webp_dec: webpDec,
  squoosh_oxipng_bg: oxipng,
  squoosh_resize_bg: resize,
}

const workerWasmModules: WasmModules = {
  load: (name) => Promise.resolve(MODULES[name]),
}

export const backend = createWasmBackend(workerWasmModules)

load may be asked for the same name more than once in principle, but the backend’s own codec initialisers are memoised, so in practice each name is requested at most once per backend instance — a host-side cache (the way nodeBackend() memoises compiled modules) is belt-and- braces, not a requirement WasmModules itself imposes.

Once you have a PixelBackend, it’s used exactly like nodeBackend()’s or canvasBackend()’s — hand it to runJob as ctx.backend. See Run a job.

Was this page helpful?