WebGPU works in dedicated, shared and service workers, and a canvas transferred to a worker as an OffscreenCanvas lets the worker own the whole render loop:
<canvas id="shelf" width="480" height="120"></canvas>
<script type="module">
const workerCode = `onmessage = async ({ data: canvas }) => {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
context.configure({ device, format: navigator.gpu.getPreferredCanvasFormat() });
const encoder = device.createCommandEncoder(); // recorded entirely in the worker
const view = context.getCurrentTexture().createView();
encoder.beginRenderPass({ colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store',
clearValue: [0.08, 0.40, 0.75, 1] }] }).end();
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
postMessage(self.constructor.name + ' drew on ' + adapter.info.vendor);
};`;
const blob = new Blob([workerCode], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
const offscreen = shelf.transferControlToOffscreen(); // the canvas now belongs to the worker
worker.postMessage(offscreen, [offscreen]);
worker.onmessage = ({ data }) => console.log(data);
</script>DedicatedWorkerGlobalScope drew on nvidia
The limit today is sharing: posting a GPUDevice to a worker throws a DataCloneError (checked in Chrome 154 1 ), so each worker needs its own device, and two threads cannot record into one device in parallel the way Vulkan or Direct3D 12 command lists can. OffscreenCanvas builds the full pattern.