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:
| M | README.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.