WebAssembly threads

Use this when the page needs SharedArrayBuffer: threaded WebAssembly, or a Worker that works on memory the page also reads.

Browsers expose SharedArrayBuffer only to a cross-origin isolated page. The page earns that from two response headers on the HTML document, Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Ecopages sets headers from the headers option, and builds a Worker whose URL is written the way the Web Workers guide describes.

Serve the headers

A rule is a source pattern and the headers to add. /** matches every path, so the page, its scripts and the Worker script all carry them.

	headers: [
		{
			source: '/**',
			headers: {
				'Cross-Origin-Opener-Policy': 'same-origin',
				'Cross-Origin-Embedder-Policy': 'require-corp',
			},
		},
	],

The headers option reference lists the pattern syntax and how rules combine: Response headers.

Start the Worker

The page script allocates shared memory, checks that the page is isolated, and hands the memory to a Worker. The memory is a WebAssembly.Memory created with shared: true, the same object a threaded WebAssembly module imports; its buffer is a SharedArrayBuffer.

import type { CounterRequest } from './counter.worker';
 
const output = document.querySelector<HTMLElement>('[data-result]');
 
function show(text: string): void {
	if (output) output.textContent = text;
}
 
if (!self.crossOriginIsolated) {
	show('not isolated');
} else {
	const memory = new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true });
	const worker = new Worker(new URL('./counter.worker.ts', import.meta.url), { type: 'module' });
 
	worker.addEventListener('message', () => {
		show(`isolated, counter ${Atomics.load(new Int32Array(memory.buffer), 0)}`);
	});
	worker.addEventListener('error', (event) => show(`worker error ${event.message}`));
	worker.postMessage({ memory, increments: 1000 } satisfies CounterRequest);
}

Declare the script on the Page so the build bundles it:

import { eco } from '@ecopages/core';
 
export default eco.page({
	metadata: () => ({ title: 'WebAssembly threads', description: 'Shared memory between the page and a Worker.' }),
	dependencies: { scripts: ['../lib/start-counter.ts'] },
	render: () => (
		<main>
			<h1>WebAssembly threads</h1>
			<output data-result>waiting</output>
		</main>
	),
});

The Worker adds to the counter with Atomics.add, which is safe while other threads touch the same cell:

export type CounterRequest = { memory: WebAssembly.Memory; increments: number };
 
addEventListener('message', (event: MessageEvent<CounterRequest>) => {
	const counter = new Int32Array(event.data.memory.buffer);
	for (let index = 0; index < event.data.increments; index += 1) {
		Atomics.add(counter, 0, 1);
	}
	postMessage('done');
});

ecopages build writes the Worker as its own hashed file under dist/client/assets and rewrites the URL to it, and ecopages dev serves it through the dev transform server.

For a real threaded module, instantiate it in each Worker with this memory as its imported env.memory. The fixture stops short of shipping a .wasm binary so it stays free of a toolchain.

What the test checks

The fixture's end-to-end test opens the page under ecopages dev and under ecopages preview, in a browser. It asserts that the page response carries both headers, that self.crossOriginIsolated is true, that the Worker script response carries Cross-Origin-Embedder-Policy as well, and that the page shows a counter of 1000 written by the Worker.

Caveats

  • Without isolation the code does not fall back. self.crossOriginIsolated is false when a header is missing, and new SharedArrayBuffer throws. The fixture shows not isolated in that case.
  • Cross-origin resources break. With require-corp, every cross-origin image, script, font or iframe the page loads must send Cross-Origin-Resource-Policy: cross-origin or a matching CORS response, or the browser blocks it. Same-origin files are unaffected.
  • Scope the rule wider than the page. The Worker script is loaded under the page's policy, so it needs the embedder header too. A rule on /** covers it; a rule limited to HTML paths would not.
  • A static export carries no headers. ecopages build output served by another host has none of this. Set the same two headers on the host, for example in a _headers file or the platform settings. See Deployment.
  • The Worker URL has rules. It must be a string literal starting with ./ or ../ inside the new Worker(...) call, with { type: 'module' }, as the Web Workers guide lists.