Browser and Web Worker Guide
Use @imagemin-rs/wasm when image bytes already live in a browser and the application does not need Node.js paths, globbing, native bindings, or executable sidecars. The package is asynchronous and memory-only: it accepts and returns Uint8Array values.
This guide covers a direct call for small jobs and a production-oriented module Worker for responsive applications. The complete runnable project is in examples/browser-wasm.
Choose the runtime
| Requirement | Browser WASM | Node package |
|---|---|---|
| In-memory GIF, PNG, or SVG | Yes | Yes |
| File paths, globs, destination folders | No | Yes |
giflossless, oxipng, optipng | Yes | Yes |
svgm | Yes | Yes |
| Gifsicle, pngquant, JPEG, WebP, AVIF | No | Yes |
| Hard cancellation | Terminate a Web Worker | Process/worker policy varies |
| Browser-local processing | Yes; no upload is required | No; runs in the Node process |
Install
pnpm add @imagemin-rs/wasmDirect use for a small job
Calling the API on the main thread is convenient for a small image or a controlled one-off task. Initialize once, pass bytes to optimize(), and turn the result back into a Blob if the browser should display or download it.
import { giflossless, initWasm, optimize, oxipng, svgm } from "@imagemin-rs/wasm";
import type { ImageKind } from "./messages";
const initialized = initWasm();
export async function optimizeOnMainThread(
bytes: Uint8Array,
kind: ImageKind,
signal?: AbortSignal,
): Promise<Uint8Array> {
await initialized;
const plugin =
kind === "gif"
? giflossless({ strip: true })
: kind === "svg"
? svgm({ preset: "safe" })
: oxipng({ optimizationLevel: 3, strip: "safe" });
const result = await optimize(bytes, {
plugins: [plugin],
...(signal === undefined ? {} : { signal }),
});
return result.data;
}AbortSignal rejects queued work and is checked between plugins. A codec call that is already executing inside WebAssembly is synchronous and cannot be preempted by the signal. Use a Worker when the page must remain responsive or when cancellation must stop active computation.
Run the complete Worker example
From a repository checkout:
pnpm install
pnpm example:browser:devOpen the local URL printed by Vite, select a PNG, animated GIF, or SVG, then optimize and download it. To verify the production bundle:
pnpm example:browser:buildThe build checks TypeScript and emits the main JavaScript, a separate module Worker, and a hashed .wasm asset. The example transfers ArrayBuffer ownership between threads so image data is not copied by structured clone.
import "./style.css";
import type { ImageKind, OptimizeRequest, OptimizeResponse } from "./messages";
const form = requiredElement<HTMLFormElement>("optimizer");
const input = requiredElement<HTMLInputElement>("image");
const optimizeButton = requiredElement<HTMLButtonElement>("optimize");
const cancelButton = requiredElement<HTMLButtonElement>("cancel");
const status = requiredElement<HTMLElement>("status");
const metrics = requiredElement<HTMLElement>("metrics");
const inputBytes = requiredElement<HTMLElement>("input-bytes");
const outputBytes = requiredElement<HTMLElement>("output-bytes");
const codec = requiredElement<HTMLElement>("codec");
const download = requiredElement<HTMLAnchorElement>("download");
let activeRequestId: number | undefined;
let downloadUrl: string | undefined;
let nextRequestId = 0;
let selectedFile: File | undefined;
let worker: Worker | undefined;
form.addEventListener("submit", async (event) => {
event.preventDefault();
const file = input.files?.[0];
if (file === undefined) {
setStatus("Choose a PNG, GIF, or SVG first.", true);
return;
}
const kind = imageKind(file);
if (kind === undefined) {
setStatus("This example accepts PNG, GIF, and SVG files.", true);
return;
}
setBusy(true);
clearDownload();
selectedFile = file;
const id = ++nextRequestId;
activeRequestId = id;
try {
const bytes = await file.arrayBuffer();
if (activeRequestId !== id) return;
const request: OptimizeRequest = { bytes, id, kind };
getWorker().postMessage(request, [bytes]);
setStatus(`Optimizing ${file.name}…`);
} catch (error) {
finishWithError(error);
}
});
cancelButton.addEventListener("click", () => {
if (activeRequestId === undefined) return;
resetWorker();
activeRequestId = undefined;
setBusy(false);
setStatus("Optimization canceled. The Worker was terminated.");
});
window.addEventListener("pagehide", () => {
resetWorker();
clearDownload();
});
function getWorker(): Worker {
if (worker !== undefined) return worker;
worker = new Worker(new URL("./image-worker.ts", import.meta.url), {
name: "imagemin-rs-example",
type: "module",
});
worker.addEventListener("message", onWorkerMessage);
worker.addEventListener("error", (event) => {
finishWithError(event.error instanceof Error ? event.error : new Error(event.message));
resetWorker();
});
return worker;
}
function onWorkerMessage(event: MessageEvent<OptimizeResponse>): void {
const response = event.data;
if (response.id !== activeRequestId) return;
activeRequestId = undefined;
setBusy(false);
if (!response.ok) {
const prefix = [response.code, response.plugin].filter(Boolean).join(" / ");
setStatus(`${prefix ? `${prefix}: ` : ""}${response.message}`, true);
return;
}
const file = selectedFile;
if (file === undefined) return;
const blob = new Blob([response.bytes], { type: outputMimeType(file) });
downloadUrl = URL.createObjectURL(blob);
download.href = downloadUrl;
download.download = outputName(file.name);
download.hidden = false;
inputBytes.textContent = formatBytes(response.inputBytes);
outputBytes.textContent = formatBytes(response.outputBytes);
codec.textContent = response.codec;
metrics.hidden = false;
setStatus(`Finished ${file.name}.`);
}
function finishWithError(error: unknown): void {
activeRequestId = undefined;
setBusy(false);
setStatus(error instanceof Error ? error.message : String(error), true);
}
function resetWorker(): void {
worker?.terminate();
worker = undefined;
}
function clearDownload(): void {
if (downloadUrl !== undefined) URL.revokeObjectURL(downloadUrl);
downloadUrl = undefined;
download.hidden = true;
download.removeAttribute("href");
metrics.hidden = true;
}
function setBusy(value: boolean): void {
input.disabled = value;
optimizeButton.disabled = value;
cancelButton.disabled = !value;
}
function setStatus(message: string, isError = false): void {
status.textContent = message;
status.dataset["error"] = String(isError);
}
function imageKind(file: File): ImageKind | undefined {
if (file.type === "image/png" || /\.png$/iu.test(file.name)) return "png";
if (file.type === "image/gif" || /\.gif$/iu.test(file.name)) return "gif";
if (file.type === "image/svg+xml" || /\.svg$/iu.test(file.name)) return "svg";
return undefined;
}
function outputMimeType(file: File): string {
const kind = imageKind(file);
if (kind === "gif") return "image/gif";
if (kind === "png") return "image/png";
if (kind === "svg") return "image/svg+xml";
return "application/octet-stream";
}
function outputName(name: string): string {
const dot = name.lastIndexOf(".");
return dot > 0 ? `${name.slice(0, dot)}.optimized${name.slice(dot)}` : `${name}.optimized`;
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
return `${(bytes / 1024).toFixed(2)} KB`;
}
function requiredElement<ElementType extends HTMLElement>(id: string): ElementType {
const element = document.querySelector(`#${id}`);
if (!(element instanceof HTMLElement)) throw new Error(`Missing #${id}`);
return element as ElementType;
}import {
giflossless,
initWasm,
optimize,
oxipng,
svgm,
type ImageminPlugin,
} from "@imagemin-rs/wasm";
import type { ImageKind, OptimizeFailure, OptimizeRequest, OptimizeSuccess } from "./messages";
const workerScope = globalThis as unknown as DedicatedWorkerGlobalScope;
workerScope.onmessage = async (event: MessageEvent<OptimizeRequest>) => {
const { bytes, id, kind } = event.data;
try {
await initWasm();
const result = await optimize(new Uint8Array(bytes), {
plugins: [pluginFor(kind)],
});
const output = toArrayBuffer(result.data);
const response: OptimizeSuccess = {
bytes: output,
codec: result.steps[0]?.plugin ?? kind,
id,
inputBytes: result.inputBytes,
ok: true,
outputBytes: result.outputBytes,
};
workerScope.postMessage(response, [output]);
} catch (error) {
const response: OptimizeFailure = {
id,
message: error instanceof Error ? error.message : String(error),
ok: false,
...readErrorContext(error),
};
workerScope.postMessage(response);
}
};
function pluginFor(kind: ImageKind): ImageminPlugin {
if (kind === "gif") return giflossless({ strip: true });
if (kind === "svg") return svgm({ preset: "safe" });
return oxipng({ optimizationLevel: 3, strip: "safe" });
}
function readErrorContext(error: unknown): { code?: string; plugin?: string } {
if (error === null || typeof error !== "object") return {};
const context: { code?: string; plugin?: string } = {};
if ("code" in error && typeof error.code === "string") context.code = error.code;
if ("plugin" in error && typeof error.plugin === "string") context.plugin = error.plugin;
return context;
}
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
}The main thread owns DOM state, downloads, and Worker lifetime. The Worker owns WASM initialization and codec execution. worker.terminate() is the hard cancellation boundary; a later request creates a clean Worker and initializes the runtime again.
File input and download flow
The runnable example follows this browser-only path:
File.arrayBuffer()reads the selected local file.postMessage(request, [bytes])transfers the buffer to the Worker.- The Worker wraps it in
Uint8Arrayand runs the matching built-in plugin. - The optimized buffer is transferred back to the main thread.
BlobandURL.createObjectURL()provide a local download.- The previous object URL is revoked before another result is exposed.
Nothing in that flow uploads the image. If application code sends the bytes to a server, that is a separate application behavior rather than a requirement of @imagemin-rs/wasm.
Initialization and asset control
For Vite and other bundlers that understand package assets, the normal path is:
import { initWasm } from "@imagemin-rs/wasm";
await initWasm();The generated loader locates the adjacent hashed WASM asset. Applications that copy the WASM binary to a controlled URL can pass the response explicitly:
const response = await fetch("/assets/imagemin_wasm_core_bg.wasm");
if (!response.ok) throw new Error(`WASM request failed: ${response.status}`);
await initWasm(response);initWasm() also accepts a URL, byte buffer, or compiled WebAssembly.Module. Concurrent calls share one initialization promise. A failed attempt may be retried after the asset problem is fixed.
Deployment checklist
- Deploy the Worker chunk and
.wasmasset together with the application JavaScript; do not copy only the entry chunk. - Serve
.wasmasapplication/wasm. The loader can fall back to buffered instantiation, but the correct MIME type enables streaming compilation. - When assets use another origin, allow that origin through CORS and verify that redirects preserve the headers.
- Cache content-hashed Worker/WASM assets as immutable. Do not apply the same long-lived policy to HTML that points at those hashes.
- Include the Worker and WASM locations in the application's CSP. Exact
worker-src,script-src, and fetch policy depends on the deployment; test the production header rather than disabling CSP. - In SSR frameworks, create the Worker only on the client—for example in a mounted hook or event handler.
windowandWorkerdo not exist during server rendering. - Validate the final deployed URL in Chromium, Firefox, and WebKit rather than relying only on the development server.
Errors and recovery
Catch ImageminError and record its stable code plus optional plugin:
| Code | Meaning |
|---|---|
ERR_IMAGEMIN_WASM_LOAD | JS glue or the WASM asset failed to load. |
ERR_IMAGEMIN_INVALID_INPUT | Input is not bytes or exceeds the size limit. |
ERR_IMAGEMIN_INVALID_OPTIONS | A plugin option or signal is invalid. |
ERR_IMAGEMIN_CODEC | The selected codec rejected the image. |
ERR_IMAGEMIN_ABORTED | Work was aborted between executable steps. |
ERR_IMAGEMIN_PLUGIN | A custom browser plugin failed. |
After a load failure, fix the URL, MIME, CORS, or CSP problem and call initWasm() again. After terminating a Worker, discard outstanding request IDs and create a new Worker before accepting more jobs, as the example does.
Supported browser profiles
| Factory | Input | Notes |
|---|---|---|
giflossless() | GIF | Preserves animation frames and timing. |
oxipng() | PNG | Lossless; never keeps a larger result. |
optipng() | PNG | OptiPNG-compatible options backed by the Rust profile. |
svgm() | SVG | Bounded safe/default native SVG profiles. |
JPEG, WebP, AVIF, Gifsicle, and pngquant remain Node-only in 1.0. The Browser WASM API documents every option and the exact runtime boundary. The Playground is a deployed Worker-based application that can be used for a quick manual check.