Back to Playwright

libwebp → WASM (self-built)

utils/libwebp-wasm/README.md

1.62.05.3 KB
Original Source

libwebp → WASM (self-built)

The build tooling for a self-contained WebP codec compiled from libwebp to WebAssembly, built entirely from source with no third-party npm codec dependency. Running build.sh produces the codec artifacts and emits them into the shipped location, packages/utils/webp/.

This folder (utils/libwebp-wasm/) — build tooling only:

FileWhat
build.shReproducible build: fetches emsdk + libwebp, compiles, links, regenerates the license.
webp_wasm.cTiny C wrapper over libwebp's config-based encode + WebPDecodeRGBA + WebPFree.

Shipped codec — packages/utils/webp/ (importable as @utils/webp/webp):

FileWhat
webp.tsTyped synchronous API: encodeWebp(image, {quality|lossless}) / decodeWebp(buffer).
webp_codec.js / webp_codec.wasmBuilt artifacts (Emscripten CommonJS glue + binary). SIMD, synchronous.
webp_codec.LICENSEThird-party license, regenerated by the build: libwebp BSD from the pinned source + Emscripten's license copied from the compiler.

Pinned: libwebp 3757b8a (a post-1.6.0 main commit — see below), built with Emscripten (tested emcc 6.0.2).

API

Exported from the @utils barrel; synchronous — no await (see "Synchronous" below):

ts
import { encodeWebp, decodeWebp } from '@utils/webp/webp';

// Lossy (quality is the 0..100 quality factor):
const webp = encodeWebp({ data: rgba, width, height }, { quality: 80 });   // -> Buffer
// Lossless (pixel-exact; quality is the 0..100 compression effort):
const exact = encodeWebp({ data: rgba, width, height }, { lossless: true });

const { data, width, height } = decodeWebp(webp);                          // RGBA Buffer

Lossless output is verified pixel-exact (decode MAE 0.0000). webp.ts reads webp_codec.wasm next to itself (shipped into lib/ by the build), instantiating lazily on first call.

Build

bash
./build.sh            # SIMD build (default) — ~552 KB wasm
SIMD=0 ./build.sh     # scalar build       — ~333 KB wasm, ~2x slower encode

The first run downloads and activates emsdk under .build/ (set EMSDK_DIR to reuse an existing one). libwebp is built with all CLI tools, threads, mux and demux disabled — just the encoder + decoder + dsp needed for RGBA <-> WebP.

Performance (photo 1920×1080, quality 80)

Buildencodedecode
SIMD (default)~370 ms~75 ms
scalar (SIMD=0)~720 ms~75 ms

The SIMD build (SSE2 → wasm SIMD) is ~2× faster on encode. Output is standard WebP; lossless round-trips pixel-exact.

Why a pinned post-1.6.0 commit (and not v1.6.0)

Building the v1.6.0 release with a modern Emscripten needed two workarounds; both were fixed upstream after 1.6.0 (released 2025-07-08) but are not in any tagged release yet, so we pin a main commit that includes them:

  1. Runtime SIMD dispatch. libwebp's wasm SIMD selector (src/dsp/cpu.c) was guarded by the legacy defined(EMSCRIPTEN) macro, which modern emcc no longer defines (only __EMSCRIPTEN__) — so the compiled SSE2 code was never selected at runtime and you silently got scalar speed. Fixed by 5339483 "dsp/cpu.c: avoid use of legacy EMSCRIPTEN definition" (2026-04-12).

  2. SIMD compile on Emscripten. libwebp's cmake probed -mavx2/-msse4.1 (which emcc accepts) and those codepaths failed to compile. Fixed by 453a18c "cmake/cpu.cmake: fix SSE2 check on Emscripten" and b51704f "dsp/cpu.c: allow use of AVX2 on Emscripten" (2026-04-24).

(Older toolchains sidestep both: Emscripten ≤ 2.0.34 still defined the legacy EMSCRIPTEN macro, and older libwebp lacked the AVX2/SSE41 probes.) Switch LIBWEBP_COMMIT in build.sh to a release tag once one ships with these fixes (expected 1.6.1 / 1.7.0).

Synchronous

The module is linked with -sWASM_ASYNC_COMPILATION=0 and CommonJS output (no EXPORT_ES6), so it instantiates with new WebAssembly.Instance and the exports attach to the module object during the (synchronous) factory call — encodeWebp / decodeWebp are then plain synchronous calls. This matches playwright-core's synchronous image Comparator, and the CommonJS glue inlines cleanly into coreBundle (via @utils).

Synchronous instantiation is Node-only in practice: browsers cap synchronous WASM compilation on the main thread (a large module must be compiled async or in a worker). Playwright uses it server-side in Node, so this is fine.

Packaging

The codec ships with playwright-core:

  • webp_codec.js (glue) is inlined into coreBundle.js via the @utils barrel.
  • webp_codec.wasm and webp_codec.LICENSE are copied into lib/ by utils/build/build.js (copyFiles) and included by packages/playwright-core/.npmignore (!lib/**/*.wasm, !lib/**/*.LICENSE). ThirdPartyNotices.txt already covers per-lib/ license sidecars generically.

webp.ts reads webp_codec.wasm via path.join(__dirname, 'webp_codec.wasm'), which resolves to lib/ at runtime (where coreBundle.js lives).