go-libwebp

Experimental translation from libwebp to Go source.
Log | Files | Refs | README | LICENSE

commit 1ca3897e4a6b5f0c06b0308583e65ee9cbb25d0e
parent 4c5b328b64936ef33eab2764e83994974b100691
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Sun, 20 Sep 2026 09:12:10 -0300

README: document the backends, build tags and benchmarks

Tag matrix with measured binary sizes, benchmark results across the corpus in
./bench, and the commands to reproduce them.

Diffstat:
MREADME.md | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 92 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md @@ -7,16 +7,88 @@ It does _not_ wrap the entire libwebp API surface. It _does_ provide a way to avoid CGO. This package provides several options for interaction with libwebp: -1. transpiled C to Go (ccgo) -2. dynamic libraries (purego) +1. dynamic libraries (purego) +2. WebAssembly (wazero) +3. transpiled C to Go (ccgo) Each option is called a "backend". Package dynamic provides a dynamic binding to libwebp. This requires the consumer to acquire the shared objects. -Package transpiled provides a Go translation of the libwebp source. This typically executes slower. +Package wasm runs libwebp as a sandboxed WebAssembly module under wazero, from a single +embedded artifact that serves every platform. +Package transpiled provides a Go translation of the libwebp source. If the shared objects are available at runtime, dynamic bindings will be preferred. +## Backends + +Which backends are compiled in is a build-time choice, so a binary carries only what it uses. + +| build | backends | binary | +| ---------------------------- | ------------------------ | ------- | +| *(default)* | dynamic, then wasm | 5.7 MiB | +| `-tags transpiled` | dynamic, then transpiled | 3.4 MiB | +| `-tags nowasm` | dynamic only | 1.8 MiB | +| `-tags nodynamic` | wasm only | 5.7 MiB | +| `-tags nodynamic,transpiled` | transpiled only | 3.3 MiB | +| `-tags nodynamic,nowasm` | none — build error | — | + +Sizes are for a trivial program built with `-ldflags='-s -w'`; a bare one is 1.4 MiB. +Most of the wasm backend's footprint is wazero's compiler, not the 0.6 MiB module. + +The `transpiled` tag displaces the wasm backend rather than adding to it, so no build +carries both pure-Go fallbacks. + +`webp.Backend()` reports which backend is live. The dynamic backend binds whatever +libwebp the host provides, which may be a different version than this package was built +against, and may encode to different bytes. + +Tags select what is linked, not what is downloaded: the module requires wazero, purego +and `modernc.org/libc` regardless. + +## Performance + +Geomean over the 14-image corpus in `./bench`, relative to dynamic, on a Ryzen 9 7900X3D. +Lower is better. + +| operation | dynamic | wasm | transpiled | +| --------------------- | ------- | ---- | ---------- | +| encode lossless | 1.0x | 2.6x | 2.5x | +| encode lossy (q=0.75) | 1.0x | 2.7x | 4.2x | +| decode | 1.0x | 2.3x | 4.5x | + +Megapixels per second, 1280x720 photographic: + +| operation | dynamic | wasm | transpiled | +| --------------------- | ------- | ---- | ---------- | +| encode lossless | 4.0 | 1.9 | 2.1 | +| encode lossy (q=0.75) | 26.2 | 9.9 | 5.8 | +| decode | 328 | 139 | 68 | + +Same source, six builds. SIMD accounts for the whole wasm/transpiled difference, and +lossless barely uses it: + +| operation | native SIMD | native scalar | wasm zig | wasm em | wasm em+SIMD | transpiled | +| --------------- | ----------- | ------------- | -------- | ------- | ------------ | ---------- | +| encode lossless | 1.0x | 1.2x | 3.2x | 3.2x | 2.6x | 2.5x | +| encode lossy | 1.0x | 2.3x | 4.9x | 4.7x | 2.7x | 4.2x | +| decode | 1.0x | 2.3x | 5.3x | 5.2x | 2.3x | 4.5x | + +All backends encode byte-identically; the harness checks that before measuring. + +Reproduce: + +``` +tools/build-native-bench-libs.sh # native shared objects to compare against +tools/build-wasm-emscripten.sh # SIMD and scalar modules +tools/build-wasm.sh # zig scalar module + +go run ./bench -so bench/lib -wasm \ + "wasm-zig=bench/lib/libwebp-zig-scalar.wasm,\ +wasm-em=bench/lib/libwebp-em-scalar.wasm,\ +wasm-em-simd=bench/lib/libwebp-em-simd.wasm" +``` + Package transpiled is a [`ccgo`-based](https://pkg.go.dev/modernc.org/ccgo/v3) translation from [libwebp](https://github.com/webmproject/libwebp/) in the hope to bring webp encoder into Go space. @@ -31,6 +103,23 @@ Can consume JPEG and PNG images. There exists a webp decoder for Go `golang.org/x/image/webp` that does not include an encoder. Using it is obtuse: if you need an encoder you need to hack together another dependency. This package extends `golang.org/x/image/webp` with an encoder translated from the libwebp project, providing a single package webp codec. +## WebAssembly + +`lib/wasm/webp/libwebp.wasm` is checked in, built from the vendored `c-lib` source. Two +build scripts are kept, both reproducing a module wazero can run: + +- `tools/build-wasm-emscripten.sh` — produces the checked-in module. Emscripten is + required for SIMD specifically: libwebp has no simd128 DSP backend, so its SSE2/SSE4.1 + kernels are reached through Emscripten's SSE-to-simd128 compat headers, which wasi-sdk + and zig do not ship. Also emits a scalar module for comparison. +- `tools/build-wasm.sh` — a zig-only scalar build. No emsdk needed; useful as a + lighter-weight toolchain and as a control when comparing. + +SIMD roughly halves lossy-encode and decode time but does nothing for lossless encode, +which is entropy-coding bound. + +Run `tools/test-matrix.sh` to build and test every supported tag combination. + ## Toolchain The transpilation process relies on a cross compile toolchain.