Cross-Origin Storage (COS)¶
Note
This feature is experimental. The underlying Cross-Origin Storage
browser API is a WICG
proposal that has not yet shipped in any browser. Emscripten’s support is
provided as a progressive enhancement — the runtime falls back to the
standard fetch() path automatically when the browser does not expose
the API.
Overview¶
The Cross-Origin Storage (COS) API is a proposed browser standard that allows web applications on different origins to share large cached files, identified by their cryptographic hashes. A file stored in COS by one site can be retrieved by any other site using the same hash, eliminating redundant downloads.
Emscripten’s CROSS_ORIGIN_STORAGE flag integrates this into the
standard Wasm loading path. At build time, Emscripten computes the SHA-256
hash of the final .wasm binary. At runtime, the generated JavaScript
tries to retrieve the compiled Wasm module from COS before falling back to
a normal network fetch. If the module is not yet in COS it is stored there
after download, making it available to other origins immediately.
When to use this flag¶
COS only delivers a benefit when the .wasm binary is byte-identical
across many different origins — that is, a popular library whose compiled
binary is loaded by many independent sites. If every visitor to every site
downloads the exact same bytes, COS means they only download it once, ever.
Good candidates are libraries or toolkits that are:
popular enough that many independent sites load the same binary,
distributed as a stable, version-pinned
.wasmfile, anda single primary
.wasmfile (COS only covers the binary that Emscripten compiles; any additional Wasm files loaded at runtime are not covered).
Do not enable this flag for application-specific Wasm code built for your own site. That binary is unique to you; no other origin will ever have the same hash, so it will never get a COS cache hit. The normal HTTP cache already handles per-origin caching efficiently.
The exception is a Wasm binary that you deploy across multiple origins you
own — for example, the same library shared between https://app.example.com
and https://api.example.com. In that case COS can eliminate the redundant
download between your own origins. Use CROSS_ORIGIN_STORAGE_ORIGINS to
restrict access to only those origins rather than opening the cache entry to
the world.
Usage¶
Pass CROSS_ORIGIN_STORAGE at link time:
emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE
Controlling which origins can read the cached file¶
The CROSS_ORIGIN_STORAGE_ORIGINS setting controls the origins field
passed to requestFileHandles() on the write (cache-miss) path. It has no
effect on the read (cache-hit) path. Three modes are available:
Globally available (default, no explicit setting needed) — any origin can retrieve the file. This is applied automatically when CROSS_ORIGIN_STORAGE is used without specifying CROSS_ORIGIN_STORAGE_ORIGINS:
emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE
Use this for popular binaries loaded by many independent origins. This is the recommended mode for resources where global COS cache hits are expected.
Restricted to a specific set of origins — only the listed origins can retrieve the file:
emcc hello.cpp -o hello.js \
-sCROSS_ORIGIN_STORAGE \
-sCROSS_ORIGIN_STORAGE_ORIGINS=https://app.example.com,https://api.example.com
Use this for proprietary resources shared across a controlled set of related
sites. Each entry must be a valid serialized HTTPS origin (scheme + host +
optional port, no path). Mixing '*' with explicit origins is a
link-time error.
Same-site only — pass an explicit empty list to omit the origins
field, making the file available only to same-site origins:
emcc hello.cpp -o hello.js \
-sCROSS_ORIGIN_STORAGE \
-sCROSS_ORIGIN_STORAGE_ORIGINS=[]
Use this for resources that should be shared across subdomains of a single site but not beyond.
Note
The COS spec defines a visibility upgrade rule: a resource’s
availability can be widened but never narrowed. If a resource is already
stored as globally available ('*'), any subsequent attempt to store it
with a more restrictive origins list is ignored by the browser.
This rule also has a security implication: because storing always requires writing the actual bytes of the resource, no third party can probe the cache to determine whether a restricted-origin entry was previously stored by another origin. A cache hit is only possible after an explicit write that provided the content, so COS cannot be used as a timing oracle to detect the presence of a resource that the probing origin cannot access.
Requirements and restrictions¶
The flag emits a warning when the target environment does not include the web (
-sENVIRONMENT=node,-sENVIRONMENT=shell):navigator.crossOriginStorageis a browser API and is never available in those environments.It produces a hard link-time error in SINGLE_FILE mode (
-sSINGLE_FILE): the Wasm binary is embedded directly into the JS output and has no standalone.wasmfile or fetchable URL to key the hash on.It produces a hard link-time error with
-sWASM_ASYNC_COMPILATION=0: the synchronous instantiation path bypassesinstantiateAsync()entirely, so the COS code can never be reached.It covers only the primary ``.wasm`` file. Secondary files produced by
-sSPLIT_MODULE(.deferred.wasm) and side modules loaded at runtime viadlopenin-sMAIN_MODULEbuilds are fetched through the normal network path and are not stored in or retrieved from COS. A warning is emitted for both of these combinations.The COS API is a progressive enhancement. Browsers without the API continue to load the Wasm module via the normal
fetch()andWebAssembly.instantiateStreaming()path without any error.
How it works¶
Build time¶
After all optimizations — including any wasm-opt passes run by Binaryen
— Emscripten reads the final .wasm binary and hashes it. The hash
object is embedded in the generated JavaScript glue as a build-time
constant (currently SHA-256):
Module['wasmHash'] = { algorithm: 'SHA-256', value: 'a3f2...c9d1' };
No extra files are produced; the hash is part of the regular .js output.
Warning
The hash is computed over the .wasm binary as emcc produces it,
after emcc’s own internal Binaryen/wasm-opt pass. If your build
pipeline runs additional wasm post-processing tools after emcc exits —
for example, an external wasm-strip or wasm-opt invocation in a
Makefile or CI script — those tools change the binary and invalidate the
embedded hash.
In that case you must recompute the hash of the final .wasm and
patch the value string in the generated .js yourself before shipping.
A minimal shell snippet for doing so (SHA-256):
# After all post-processing is complete:
final_hash=$(sha256sum hello.wasm | awk '{print $1}')
sed -i "s/'[0-9a-f]\{64\}'/'${final_hash}'/g" hello.js
On macOS, use shasum -a 256 in place of sha256sum, and install
GNU sed (brew install gnu-sed) or adapt the sed command for BSD
sed syntax.
Runtime (web only)¶
When the page loads, the generated JavaScript follows this logic:
Feature detection — check
'crossOriginStorage' in navigator. If the API is absent, skip to the normal fetch path immediately.Cache hit — call
navigator.crossOriginStorage.requestFileHandles([cosHash]). If the handle is returned (the module is already in COS), read it withhandle.getFile()→.arrayBuffer()and pass the bytes toWebAssembly.instantiate(). Then invokeModule['onCOSCacheHit'](hash)if defined.Cache miss — if a
NotFoundErroris thrown, fetch the.wasmover the network as usual, invokeModule['onCOSCacheMiss'](hash, url)if defined, callWebAssembly.instantiate()immediately so the page loads without delay, and then write the bytes into COS in the background (fire-and-forget) using theoriginsvalue controlled by CROSS_ORIGIN_STORAGE_ORIGINS ('*'by default). Once the write completes, invokeModule['onCOSStore'](hash)if defined.Fallback — any unexpected error (
NotAllowedErrorfrom the browser, network failure during the miss path, etc.) is logged witherr()and the runtime falls through to the standard streaming-instantiation path below. The page always loads.
Instrumentation callbacks¶
Three optional Module properties let you observe COS events at runtime.
They are opt-in: to include the callback code in the output, list them in
INCOMING_MODULE_JS_API at link time:
emcc hello.cpp -o hello.js -sCROSS_ORIGIN_STORAGE \
-sINCOMING_MODULE_JS_API=onCOSCacheHit,onCOSCacheMiss,onCOSStore
var Module = {
// Called when the Wasm binary was served from the cross-origin cache.
onCOSCacheHit: (hash) => {
console.log('Cache hit, SHA-256:', hash);
},
// Called when the Wasm binary was not in COS and was fetched over the
// network. |hash| is the hash that missed; |url| is the fallback URL.
onCOSCacheMiss: (hash, url) => {
console.log('Cache miss, SHA-256:', hash, 'fetched from:', url);
},
// Called after the Wasm binary has been successfully written to COS.
onCOSStore: (hash) => {
console.log('Stored in COS, SHA-256:', hash);
},
};
Testing with the extension polyfill¶
Because no browser ships the COS API natively yet, you can experiment using
the Cross-Origin Storage extension,
which injects a navigator.crossOriginStorage polyfill on every page.
Manual testing¶
Install the extension in Chrome.
Build your project with
-sCROSS_ORIGIN_STORAGE -sENVIRONMENT=web.Serve the output over HTTP (e.g. with
emrunorpython3 -m http.server).Open the page — on the first load the Wasm binary is fetched and stored in COS. Open the same page in a second tab or from a different origin: the module is loaded from COS without a network request.
Automated browser testing¶
The Emscripten browser test suite includes COS tests that run against the
polyfill extension. The extension must be available as an unpacked
directory (containing manifest.json). A helper script downloads and
unpacks it automatically:
python3 test/setup_cos_extension.py
Then run the tests, passing the printed path as EMTEST_COS_EXTENSION_PATH:
EMTEST_COS_EXTENSION_PATH=$(python3 test/setup_cos_extension.py --quiet) \
python3 test/runner.py \
browser.test_cross_origin_storage_fallback \
browser.test_cross_origin_storage_miss_then_hit
test_cross_origin_storage_fallback does not require the extension and
verifies that a -sCROSS_ORIGIN_STORAGE build loads correctly on browsers
where the COS API is absent. test_cross_origin_storage_miss_then_hit
requires the extension and exercises both the cache-miss store and cache-hit
paths in sequence.
Verifying the embedded hash¶
You can confirm that the hash embedded in the .js output matches the
actual .wasm file using standard tools:
# SHA-256 of the wasm file
sha256sum hello.wasm
# Extract the hash embedded in the JS
grep -oP "value: '\K[0-9a-f]{64}" hello.js
Both values must be identical. The Emscripten test suite checks this
automatically via test_cross_origin_storage_js_output in
test/test_other.py.
Custom Module['instantiateWasm'] implementations¶
The COS fetch logic described above lives inside instantiateAsync(), which
is the standard Emscripten wasm loading path. When a program provides its own
Module['instantiateWasm'] callback, Emscripten calls that callback directly
and skips instantiateAsync() entirely, so the built-in COS code is never
reached.
To support COS in a custom loader, Emscripten exposes the build-time SHA-256 hash as a named Module property:
Module['wasmHash'] // { algorithm: 'SHA-256', value: '<64 hex chars>' }
This property is set by the generated JavaScript before
Module['instantiateWasm'] is called, so it is always available inside the
callback. Module in this context is the config object passed to the module
factory — whatever variable you use when calling new Module(config) or the
equivalent factory function. A custom loader can read Module['wasmHash']
via a reference to that config object:
var Module = {
instantiateWasm(imports, onSuccess) {
// `this` inside the callback is Emscripten's internal Module object;
// read the hash via the outer Module reference instead.
const cosHash = Module['wasmHash'];
if (cosHash?.value && globalThis.navigator?.crossOriginStorage) {
navigator.crossOriginStorage.requestFileHandles([cosHash])
.then(handles => handles[0].getFile())
.then(f => f.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, imports))
.then(({instance, module}) => onSuccess(instance, module))
.catch(err => {
if (err.name !== 'NotFoundError') throw err;
// cache miss — fetch normally and store in the background
fetch('hello.wasm')
.then(r => r.arrayBuffer())
.then(bytes => {
WebAssembly.instantiate(bytes, imports)
.then(({instance, module}) => onSuccess(instance, module));
// fire-and-forget store
navigator.crossOriginStorage
.requestFileHandles([cosHash], { create: true, origins: '*' })
.then(wh => wh[0].createWritable())
.then(w => w.write(new Blob([bytes], {type:'application/wasm'}))
.then(() => w.close()));
});
});
return; // async; onSuccess called above
}
// fallback — normal streaming instantiation
WebAssembly.instantiateStreaming(fetch('hello.wasm'), imports)
.then(({instance, module}) => onSuccess(instance, module));
},
};
Module['wasmHash'] is only present in builds compiled with
CROSS_ORIGIN_STORAGE. Always guard on its truthiness before using it,
as shown above, so the same loader code works in builds compiled without the
flag.
Relationship to other caching mechanisms¶
COS is a complement to, not a replacement for, existing browser caches:
HTTP cache / Service Worker cache — still used for per-origin caching. COS adds cross-origin sharing on top.
``NODE_CODE_CACHING`` — a Node.js-specific V8 bytecode cache; unrelated to COS.
IndexedDB / OPFS — per-origin storage; COS shares across origins.
See also¶
Emscripten Compiler Settings —
CROSS_ORIGIN_STORAGEentryBuilding to WebAssembly — general guide to building Wasm with Emscripten