Repository navigation
Vips: use binary (UTF-8) encoding instead of base64 when inlining WASM to shrink the worker bundle #79187
Description
What
Switch the WASM inline encoding in our build from base64 data URLs to Emscripten's compact UTF-8 binary encoding to reduce the compressed size of the @wordpress/vips worker bundle.
Background
@wordpress/vips inlines wasm-vips' .wasm binaries directly into build/modules/vips/worker.min.js so there are no separate WASM downloads and no host MIME-type issues. The inlining is done by the wasmInlinePlugin in packages/wp-build/lib/build.mjs, which currently emits data:application/wasm;base64,… data URLs.
base64 inflates binary by ~33% and — more importantly — misaligns byte patterns so it compresses poorly. As @kleisauke pointed out, Emscripten's SINGLE_FILE_BINARY_ENCODE uses a custom UTF-8 embedding instead, which compresses much better with both gzip and brotli.
Measured impact
Measured on the 0.0.18 vips.wasm + vips-heif.wasm through a faithful reproduction of the full build pipeline (inline → JSON.stringify worker-code wrapper → minified bundle):
worker.min.js |
gzip | brotli |
|---|---|---|
| base64 (current) | ~4.20 MB | ~3.31 MB |
| binary (UTF-8) | ~3.65 MB | ~2.79 MB |
| change | −13.2% | −16% |
The bundle-size check uses gzip, so this is roughly −550 kB off the compressed worker.min.js. (An earlier comment cited ~18% from a standalone payload measurement; the full-pipeline figure above is the accurate one, since the real worker.min.js wraps the worker in a JSON.stringify layer.)
Tradeoff: the uncompressed worker.min.js grows ~16% (~1.8 MB), since UTF-8 stores high byte values as two bytes vs base64's one ASCII char. The worker is lazily loaded and served compressed, so the compressed transfer win benefits users while the uncompressed cost is transient.
Proposed implementation
- Update
wasmInlinePlugininpackages/wp-build/lib/build.mjsto emit the binary-encoded string (Emscripten's scheme: write each byte as UTF-8, escaping\r,\n,\, and the chosen quote char) instead of a base64 data URL. - Update consumption in
packages/vips/src/index.ts: the binary-encoded string is not a fetchable data URL, solocateFilemust decode it to bytes and passwasm-vipseitherwasmBinary(mainvips.wasmmodule) or a Blob URL viaURL.createObjectURL(thevips-heif.wasmdynamic library, which is loaded by URL fetch). - Verify the worker still instantiates and the existing
@wordpress/vipstests pass, and confirm the bundle-size reduction in CI.
Notes
- Binary encoding requires the JS be served as UTF-8. Module scripts are always decoded as UTF-8, so the bytes round-trip correctly.
- Discovered while bumping
wasm-vipsto 0.0.18 in Vips: bump wasm-vips to 0.0.18 for high-bit-depth AVIF decoding #79179; this optimization is independent of that change.
Implemented in #79188.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fields✅ Done
Activity
kleisauke commentedon Jun 15, 2026
For context, the comment at lovell/sharp-libvips#247 (comment) may also be relevant here, as server-side Brotli compression could further reduce the size of these Wasm binaries. AFAIK, using a CDN such as jsDelivr is probably out of scope for Gutenberg or WordPress.
adamsilverstein commentedon Jun 15, 2026
Thanks, I'll give that a try. Right jsDelivr is out of scope, although we do load some 3p libraries from our own CDN, the build process and direct inclusion are a better chould for this feature.
adamsilverstein commentedon Jun 15, 2026
Thanks @kleisauke, that's a helpful reference. I measured all three options for the two inlined binaries (
vips.wasm+vips-heif.wasm, 0.0.18), through a faithful reproduction of our build pipeline:.wasm, not inlined (floor)Two takeaways:
Brotli is the dominant lever, but it's a serving-layer concern. It's not something the build controls — it depends on the web server (
mod_brotli/ngx_brotli) or CDN, which most production WordPress hosts already provide. jsDelivr is out of scope as you noted. So there's no additional build-side change to make here from the Brotli point.Binary encoding already captures nearly all of the achievable benefit under Brotli. The inlined binary-encoded artifact (~2.79 MB brotli) is within ~220 kB of the raw-
.wasmbrotli floor (~2.57 MB). Getting fully to the floor would mean serving the.wasmun-inlined, which we deliberately avoid (separate downloads + host MIME-type issues).So #79188 (binary encoding) is the build-side improvement, and serving Brotli realizes the rest at the hosting layer. Opened the PR with the full numbers and a note on the one tradeoff (uncompressed size grows ~16%).