fix: defer WASM init in bindings to first use - #673
Open
JakeChampion wants to merge 1 commit into
Open
Conversation
The wasm bindings for resvg, satori and takumi created their init promise at module-evaluation time. Bundlers that emit a single chunk for worker targets (e.g. Nitro presets with inlineDynamicImports, used by netlify-edge and other base-worker presets) hoist these modules into the entry, so the WASM fetch and compilation ran on every isolate cold start, even for requests that never render an OG image. Expose initWasmPromise as a lazy memoized getter instead: the object shape consumed by getResvg/getSatori/getTakumi is unchanged, but the init chain only starts on first access, which happens inside the render path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sibh1BhDY97CPafumYmJyU
commit: |
📦 Package Size
All tracked output (18)
Runtime dependencies (30)
Baseline: main_@_f8308f5a___2026-08-11 · gzip is the comparison metric · changes below 16 B gzip are ignored |
Collaborator
|
I'm not quite sure if this is right; the module itself is only loaded within the route handler, which loads lazily. The issue is correctly upstream if Nitro is eagerly loading. |
Author
Collaborator
|
Yup I saw that, thanks. Will keep this open until that's merged and we can verify. |
Collaborator
🤖 REVIEW PAUSED
This pull request is from an outside contributor. Add the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The wasm bindings for resvg, satori and takumi start their init chain at module-evaluation time:
This PR exposes
initWasmPromiseas a lazy, memoized getter instead. The object shape consumed bygetResvg/getSatori/getTakumiis unchanged; the init chain now only starts on the first property access, which happens inside the render path.Why
The bindings were written to be lazy at the module level -
import('#og-image/bindings/resvg')insidegetResvg()- which works when the bundler keeps dynamic imports as separate chunks. But several deployment targets bundle the server as a single file: Nitro'sbase-workerfamily setsinlineDynamicImports: true(netlify-edge, and others inherit it). Rollup then inlines the "lazy" binding module into the entry chunk, and since ESM top-level code runs at module-link time, the WASM fetch +WebAssembly.instantiatefires as soon as the isolate boots.Net effect on those targets: every cold start pays WASM compilation for resvg (and yoga for satori), including plain page loads that never render an OG image. On edge runtimes with per-request CPU budgets and frequent cold starts, this dominates cold-start latency. We measured this in production on Netlify Edge Functions, where the built
server.jscontained the resvginitWasmPromiseas a top-level const whose init chain ran unconditionally at boot.With this change, single-file bundles still contain the WASM, but compilation is deferred to the first OG-image render on that isolate.
Changes
bindings/resvg/wasm.ts,bindings/resvg/wasm-fs.tsbindings/satori/wasm.ts,bindings/satori/wasm-fs.tsbindings/takumi/wasm.tsEach replaces the eagerly-created
initWasmPromiseproperty with aget initWasmPromise()that creates the promise on first access and memoizes it. Thenode/node-devbindings (Promise.resolve()) are untouched.Verification
pnpm lintandpnpm typecheckpass.bindings/resvg/wasm.tsno longer triggers any WASM loading; the load attempt happens exactly on firstinitWasmPromiseaccess (verified by importing the module in isolation and observing the resolution attempt only fires on property access).