WebAssembly port of libavif with separate decode and encode ES modules for browsers, Node 20+, and Edge/Workers.
npm install @phenomen/libavif-wasmCall init() once per isolate with a WebAssembly.Module before decode or encode.
import { init, decode } from "@phenomen/libavif-wasm/decode";
import decodeWasm from "@phenomen/libavif-wasm/decode.wasm";
await init(decodeWasm);
const { data, width, height } = await decode(await fetch("/photo.avif").then((r) => r.arrayBuffer()));
ctx.putImageData(new ImageData(data, width, height), 0, 0);import { init, encode } from "@phenomen/libavif-wasm/encode";
import encodeWasm from "@phenomen/libavif-wasm/encode.wasm";
await init(encodeWasm);
const avif = await encode(imageData, { quality: 70, speed: 6, subsample: 420 });If your bundler gives you bytes or a URL instead of a WebAssembly.Module, compile once with new WebAssembly.Module(bytes) and pass that to init.
import { readFile } from "node:fs/promises";
import { init, decode, wasmUrl } from "@phenomen/libavif-wasm/decode";
await init(new WebAssembly.Module(await readFile(wasmUrl)));
const image = await decode((await readFile("./photo.avif")).buffer);import { readFile } from "node:fs/promises";
import { init, encode, wasmUrl } from "@phenomen/libavif-wasm/encode";
await init(new WebAssembly.Module(await readFile(wasmUrl)));
const avif = await encode(image, { quality: 60, speed: 8 });import { init, decode } from "@phenomen/libavif-wasm/decode";
import decodeWasm from "@phenomen/libavif-wasm/decode.wasm";
export const runtime = "edge";
export async function POST(req: Request) {
await init(decodeWasm);
const { width, height } = await decode(await req.arrayBuffer());
return Response.json({ width, height });
}Cloudflare Workers and Vercel Edge treat import … from "*.wasm" as a WebAssembly.Module. Configure the bundler to emit wasm as a module import.
init(wasmModule: WebAssembly.Module): Promise<void>
decode(input: ArrayBuffer | ArrayBufferView): Promise<{
data: Uint8ClampedArray
width: number
height: number
}>
encode(
image: { data: ArrayLike<number>; width: number; height: number },
options?: {
quality?: number // 0–100, default 60
speed?: number // 0 (slowest / best) – 10 (fastest), default 6
subsample?: 420 | 422 | 444 | 400
bitDepth?: 8 // only 8 is supported
},
): Promise<ArrayBuffer>Import decode from @phenomen/libavif-wasm/decode and encode from @phenomen/libavif-wasm/encode. Each has its own wasm file and init.
| Module | Raw | gzip-9 |
|---|---|---|
decode.wasm |
573.1 KiB (586,861 B) | 242.9 KiB (248,750 B) |
encode.wasm |
1.95 MiB (2,041,605 B) | 837.1 KiB (857,162 B) |
| Component | Version |
|---|---|
| libavif | v1.4.2 |
| dav1d | 1.5.4 |
| libaom | v3.15.2 |
| libyuv | 644251f25 |
| libsharpyuv (libwebp) | v1.6.0 |
| Emscripten | 6.0.12 |
Pins live in versions.env.
- Still images only (no animated AVIF).
- Decode output is 8-bit RGBA. 10- and 12-bit AVIF still decodes.
- Encode input is 8-bit RGBA, and the encoded AVIF is 8-bit.
- Single-threaded. Call
init()once per isolate.
docker build --build-arg EMSDK_VERSION=6.0.12 -t libavif-wasm-builder .
docker run --rm -v "$PWD":/src -u "$(id -u):$(id -g)" libavif-wasm-builder ./scripts/build.sh
node --test test/node/*.test.mjsOr run ./scripts/build.sh with emsdk, Meson, Ninja, CMake, and pkg-config on PATH.
This repository is MIT. The wasm binaries bundle libavif, dav1d, libaom, libyuv, and libsharpyuv. Full texts — including the Alliance for Open Media Patent License 1.0, which applies to encode — are in THIRD_PARTY_NOTICES.md.