Skip to content

Support async instantiation in Node.js - #27730

Draft
jeremy-code wants to merge 12 commits into
emscripten-core:mainfrom
jeremy-code:node-streaming
Draft

jeremy-code wants to merge 12 commits into
emscripten-core:mainfrom
jeremy-code:node-streaming

Conversation

@jeremy-code

Copy link
Copy Markdown
Contributor

Since Node.js is on v8, which optimizes instantiateStreaming significantly due to caching,123 async instantiation should be preferred. While Node.js doesn't support file URIs, a file system read stream can still be used as an input for instantiateStreaming, so as long as the MIME type is set correctly (this same technique is used in cosInstantiateStream).

Unidici does support using a Node.js read stream in the Response constructor (see here). I believe this means this should be valid in all Node.js environments. If there somehow is an issue (maybe in an edge environment where some Node polyfill exists, but the native web Response is still used...?), this could be fixed either by:

  1. Readable.toWeb() from node:stream
  2. Using fs.openAsBlob() instead (might have some weird issues on older Node.js versions since blobs used to be stored in memory in Node File-based Blob implementation nodejs/node#45188)

Let me know what you think.

Footnotes

  1. https://v8.dev/blog/wasm-code-caching

  2. https://web.dev/articles/loading-wasm

  3. MDN warns "This method is not the most efficient way of fetching and instantiating Wasm modules. If at all possible, you should use the newer WebAssembly.instantiateStreaming() method instead": https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static

This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (13) test expectation files were updated by
running the tests with `--rebaseline`:

```
codesize/test_codesize_cxx_except_wasm.json: 168663 => 168976 [+313 bytes / +0.19%]
codesize/test_codesize_cxx_except_wasm_legacy.json: 166523 => 166836 [+313 bytes / +0.19%]
codesize/test_codesize_cxx_lto.json: 119632 => 119945 [+313 bytes / +0.26%]
test/codesize/test_codesize_file_preload.expected.js updated
codesize/test_codesize_mem_O3_grow_standalone.json: 9306 => 9619 [+313 bytes / +3.36%]
codesize/test_codesize_mem_O3_standalone.json: 9140 => 9453 [+313 bytes / +3.42%]
codesize/test_codesize_mem_O3_standalone_lib.json: 8334 => 8647 [+313 bytes / +3.76%]
codesize/test_codesize_mem_O3_standalone_narg.json: 8455 => 8768 [+313 bytes / +3.70%]
codesize/test_codesize_mem_O3_standalone_narg_flto.json: 7386 => 7699 [+313 bytes / +4.24%]
codesize/test_codesize_minimal_64.json: 2439 => 2751 [+312 bytes / +12.79%]
test/codesize/test_codesize_minimal_O0.expected.js updated
codesize/test_codesize_minimal_pthreads_memgrowth.json: 26506 => 26819 [+313 bytes / +1.18%]
codesize/test_unoptimized_code_size.json: 173189 => 172730 [-459 bytes / -0.27%]

Average change: +2.98% (-0.27% - +12.79%)
```
This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (46) test expectation files were updated by
running the tests with `--rebaseline`:

```
codesize/test_codesize_cxx_ctors1.json: 153508 => 153517 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_ctors2.json: 152917 => 152926 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_except.json: 199612 => 199621 [+9 bytes / +0.00%]
codesize/test_codesize_cxx_except_wasm.json: 168976 => 168985 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_except_wasm_legacy.json: 166836 => 166845 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_lto.json: 119945 => 119954 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_mangle.json: 265942 => 265951 [+9 bytes / +0.00%]
codesize/test_codesize_cxx_noexcept.json: 155419 => 155428 [+9 bytes / +0.01%]
codesize/test_codesize_cxx_wasmfs.json: 180524 => 180533 [+9 bytes / +0.00%]
test/codesize/test_codesize_file_preload.expected.js updated
codesize/test_codesize_file_preload.json: 23714 => 23723 [+9 bytes / +0.04%]
codesize/test_codesize_files_js_fs.json: 18565 => 18574 [+9 bytes / +0.05%]
codesize/test_codesize_files_wasmfs.json: 63544 => 63553 [+9 bytes / +0.01%]
codesize/test_codesize_hello_O0.json: 38265 => 38274 [+9 bytes / +0.02%]
codesize/test_codesize_hello_O1.json: 7956 => 7965 [+9 bytes / +0.11%]
codesize/test_codesize_hello_O2.json: 5611 => 5620 [+9 bytes / +0.16%]
codesize/test_codesize_hello_O3.json: 5307 => 5316 [+9 bytes / +0.17%]
codesize/test_codesize_hello_Os.json: 5297 => 5306 [+9 bytes / +0.17%]
codesize/test_codesize_hello_Oz.json: 4935 => 4944 [+9 bytes / +0.18%]
codesize/test_codesize_hello_dylink.json: 43494 => 43503 [+9 bytes / +0.02%]
codesize/test_codesize_hello_dylink_all.json: 859299 => 859308 [+9 bytes / +0.00%]
codesize/test_codesize_hello_export_nothing.json: 2998 => 3007 [+9 bytes / +0.30%]
codesize/test_codesize_hello_wasmfs.json: 5307 => 5316 [+9 bytes / +0.17%]
codesize/test_codesize_libcxxabi_message_O3.json: 3411 => 3420 [+9 bytes / +0.26%]
codesize/test_codesize_libcxxabi_message_O3_standalone.json: 3628 => 3637 [+9 bytes / +0.25%]
codesize/test_codesize_mem_O3.json: 9455 => 9464 [+9 bytes / +0.10%]
codesize/test_codesize_mem_O3_grow.json: 9791 => 9800 [+9 bytes / +0.09%]
codesize/test_codesize_mem_O3_grow_standalone.json: 9619 => 9628 [+9 bytes / +0.09%]
codesize/test_codesize_mem_O3_standalone.json: 9453 => 9462 [+9 bytes / +0.10%]
codesize/test_codesize_mem_O3_standalone_lib.json: 8647 => 8656 [+9 bytes / +0.10%]
codesize/test_codesize_mem_O3_standalone_narg.json: 8768 => 8777 [+9 bytes / +0.10%]
codesize/test_codesize_mem_O3_standalone_narg_flto.json: 7699 => 7708 [+9 bytes / +0.12%]
codesize/test_codesize_minimal_64.json: 2751 => 2760 [+9 bytes / +0.33%]
test/codesize/test_codesize_minimal_O0.expected.js updated
codesize/test_codesize_minimal_O0.json: 20262 => 20271 [+9 bytes / +0.04%]
codesize/test_codesize_minimal_O1.json: 3565 => 3574 [+9 bytes / +0.25%]
codesize/test_codesize_minimal_O2.json: 2691 => 2700 [+9 bytes / +0.33%]
codesize/test_codesize_minimal_O3.json: 2386 => 2395 [+9 bytes / +0.38%]
codesize/test_codesize_minimal_Os.json: 2386 => 2395 [+9 bytes / +0.38%]
codesize/test_codesize_minimal_Oz-ctors.json: 2357 => 2366 [+9 bytes / +0.38%]
codesize/test_codesize_minimal_Oz.json: 2386 => 2395 [+9 bytes / +0.38%]
codesize/test_codesize_minimal_esm.json: 2517 => 2526 [+9 bytes / +0.36%]
codesize/test_codesize_minimal_pthreads.json: 26360 => 26369 [+9 bytes / +0.03%]
codesize/test_codesize_minimal_pthreads_memgrowth.json: 26819 => 26828 [+9 bytes / +0.03%]
codesize/test_codesize_minimal_wasmfs.json: 2386 => 2395 [+9 bytes / +0.38%]
codesize/test_unoptimized_code_size.json: 172730 => 172757 [+27 bytes / +0.02%]

Average change: +0.14% (+0.00% - +0.38%)
```
@jeremy-code

Copy link
Copy Markdown
Contributor Author

Regarding the Bun test failures, I am guessing it may be related to this issue: oven-sh/bun#33220.

@sbc100

sbc100 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Are you sure that the caching is working under node and using streaming instantiation? Where is the cached code stored? How can we show that the caching is working?

We do already have an existing NODE_CODE_CACHING setting: https://emscripten.org/docs/tools_reference/settings_reference.html#node-code-caching

How does this compare to that?

@jeremy-code

Copy link
Copy Markdown
Contributor Author

@sbc100

Here's a quick test repo https://github.com/jeremy-code/emscripten-node-test, and the Node.js trace logs do show that the events "wasm.StartStreamingCompilation" and "wasm.InitializeStreaming" are occurring. I'm not sure how to verify the code caching part works as intended, but at the very least, the aspect of "compil[ing] the WebAssembly module while the module bytes are still downloading" is definitely improving performance (screenshots below, no stream on top, streaming bottom).

image image

It's not easy to test the code caching part since the bottleneck is Node.js already storing the file in memory (which is not much of a concern since the Wasm binary is only 151.589 KB), but in practice, this should be GC'd eventually.

As for where the cache is stored, I believe it is in memory. Per node:module documentation, it only stores CJS/MJS/TS files and in practice, with NODE_COMPILE_CACHE set, I don't see any Wasm files.

Per the link you provided, NODE_CODE_CACHING is a legacy setting and has no effect, is that true?

This is an automatic change generated by tools/maint/rebaseline_tests.py.

The following (46) test expectation files were updated by
running the tests with `--rebaseline`:

```
codesize/test_codesize_cxx_ctors1.json: 153517 => 153546 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_ctors2.json: 152926 => 152955 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_except.json: 199621 => 199650 [+29 bytes / +0.01%]
codesize/test_codesize_cxx_except_wasm.json: 168985 => 169014 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_except_wasm_legacy.json: 166845 => 166874 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_lto.json: 119954 => 119983 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_mangle.json: 265951 => 265980 [+29 bytes / +0.01%]
codesize/test_codesize_cxx_noexcept.json: 155428 => 155457 [+29 bytes / +0.02%]
codesize/test_codesize_cxx_wasmfs.json: 180533 => 180562 [+29 bytes / +0.02%]
test/codesize/test_codesize_file_preload.expected.js updated
codesize/test_codesize_file_preload.json: 23723 => 23752 [+29 bytes / +0.12%]
codesize/test_codesize_files_js_fs.json: 18574 => 18603 [+29 bytes / +0.16%]
codesize/test_codesize_files_wasmfs.json: 63553 => 63584 [+31 bytes / +0.05%]
codesize/test_codesize_hello_O0.json: 38274 => 38304 [+30 bytes / +0.08%]
codesize/test_codesize_hello_O1.json: 7965 => 7995 [+30 bytes / +0.38%]
codesize/test_codesize_hello_O2.json: 5620 => 5649 [+29 bytes / +0.52%]
codesize/test_codesize_hello_O3.json: 5316 => 5345 [+29 bytes / +0.55%]
codesize/test_codesize_hello_Os.json: 5306 => 5335 [+29 bytes / +0.55%]
codesize/test_codesize_hello_Oz.json: 4944 => 4973 [+29 bytes / +0.59%]
codesize/test_codesize_hello_dylink.json: 43503 => 43532 [+29 bytes / +0.07%]
codesize/test_codesize_hello_dylink_all.json: 859308 => 859337 [+29 bytes / +0.00%]
codesize/test_codesize_hello_export_nothing.json: 3007 => 3036 [+29 bytes / +0.96%]
codesize/test_codesize_hello_wasmfs.json: 5316 => 5345 [+29 bytes / +0.55%]
codesize/test_codesize_libcxxabi_message_O3.json: 3420 => 3449 [+29 bytes / +0.85%]
codesize/test_codesize_libcxxabi_message_O3_standalone.json: 3637 => 3666 [+29 bytes / +0.80%]
codesize/test_codesize_mem_O3.json: 9464 => 9493 [+29 bytes / +0.31%]
codesize/test_codesize_mem_O3_grow.json: 9800 => 9829 [+29 bytes / +0.30%]
codesize/test_codesize_mem_O3_grow_standalone.json: 9628 => 9657 [+29 bytes / +0.30%]
codesize/test_codesize_mem_O3_standalone.json: 9462 => 9491 [+29 bytes / +0.31%]
codesize/test_codesize_mem_O3_standalone_lib.json: 8656 => 8685 [+29 bytes / +0.34%]
codesize/test_codesize_mem_O3_standalone_narg.json: 8777 => 8806 [+29 bytes / +0.33%]
codesize/test_codesize_mem_O3_standalone_narg_flto.json: 7708 => 7737 [+29 bytes / +0.38%]
codesize/test_codesize_minimal_64.json: 2760 => 2789 [+29 bytes / +1.05%]
test/codesize/test_codesize_minimal_O0.expected.js updated
codesize/test_codesize_minimal_O0.json: 20271 => 20301 [+30 bytes / +0.15%]
codesize/test_codesize_minimal_O1.json: 3574 => 3604 [+30 bytes / +0.84%]
codesize/test_codesize_minimal_O2.json: 2700 => 2729 [+29 bytes / +1.07%]
codesize/test_codesize_minimal_O3.json: 2395 => 2424 [+29 bytes / +1.21%]
codesize/test_codesize_minimal_Os.json: 2395 => 2424 [+29 bytes / +1.21%]
codesize/test_codesize_minimal_Oz-ctors.json: 2366 => 2395 [+29 bytes / +1.23%]
codesize/test_codesize_minimal_Oz.json: 2395 => 2424 [+29 bytes / +1.21%]
codesize/test_codesize_minimal_esm.json: 2526 => 2549 [+23 bytes / +0.91%]
codesize/test_codesize_minimal_pthreads.json: 26369 => 26398 [+29 bytes / +0.11%]
codesize/test_codesize_minimal_pthreads_memgrowth.json: 26828 => 26857 [+29 bytes / +0.11%]
codesize/test_codesize_minimal_wasmfs.json: 2395 => 2424 [+29 bytes / +1.21%]
codesize/test_unoptimized_code_size.json: 172757 => 172775 [+18 bytes / +0.01%]

Average change: +0.43% (+0.00% - +1.23%)
```
@jeremy-code

Copy link
Copy Markdown
Contributor Author

I switched it from using openAsBlob to createReadStream (converted to a web stream) since Node.js seemed to handle backpressure strangely with blob.stream() (see node_trace.1.stream-blob.log, node_trace.1.stream-web.log).

I think this also makes more sense since openAsBlob is a slightly newer API (Node v19.8.0) compared to createReadStream (Node v0.1.31) and Readable.toWeb (Node v17.0.0).

Unfortunately, it looks like there is no easy fix to the Bun issue. I'm going to file a bug report and see if a fix is possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants