For Node.js 2,131 , the webgpu package (dawn-gpu/node-webgpu (https://github.com/dawn-gpu/node-webgpu 111 ), MIT, 0.6.1) is Dawn 1,149 compiled as a native add-on with prebuilt binaries for Windows, Linux and macOS. npm 2,036 install webgpu, then call create() for a navigator.gpu equivalent. This script gives BookNest's prices a 15% discount on the GPU:
import { create, globals } from 'webgpu'; // Dawn compiled as a Node add-on
Object.assign(globalThis, globals); // GPUBufferUsage, GPUMapMode, ...
const adapter = await create([]).requestAdapter();
const device = await adapter.requestDevice();
console.log('adapter:', adapter.info.vendor, adapter.info.description);
const prices = new Float32Array([14.99, 39.5, 24, 18.75, 16.2, 21.3]); // BookNest's six
const module = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage, read_write> price: array<f32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x < arrayLength(&price)) { price[id.x] = round(price[id.x] * 85.0) / 100.0; }
}` });
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module } });
const { STORAGE, COPY_SRC, COPY_DST, MAP_READ } = GPUBufferUsage;
const data = device.createBuffer({ size: 24, usage: STORAGE | COPY_SRC | COPY_DST });
const read = device.createBuffer({ size: 24, usage: COPY_DST | MAP_READ });
device.queue.writeBuffer(data, 0, prices);
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: data } }] }));
pass.dispatchWorkgroups(1), pass.end();
encoder.copyBufferToBuffer(data, 0, read, 0, 24);
device.queue.submit([encoder.finish()]);
await read.mapAsync(GPUMapMode.READ);
console.log('15% off:', [...new Float32Array(read.getMappedRange())].map((p) => p.toFixed(2)));
device.destroy();adapter: nvidia D3D12 driver version 32.0.15.9186 15% off: [ '12.74', '33.58', '20.40', '15.94', '13.77', '18.10' ]
It ran in 2.4 seconds under Node.js 25.8.0, through Direct3D 12. Outside a web page description is filled in, and results come back through buffers, since there is no canvas: useful for server-side compute and shader tests in CI.
Deno 60,577 implements WebGPU with wgpu (wgpu): navigator.gpu becomes global with --unstable-webgpu (still listed in Deno 2.9.7's documentation), and the unstable Deno.UnsafeWindowSurface presents to a native window. Deno is not installed here, so it was not run; the script above would lose its first two lines and call navigator.gpu.requestAdapter().
<!doctype html>
<style>
body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
.stage { position: relative; width: 100%; max-width: 600px; }
.stage canvas { display: block; width: 100%; }
.stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
<canvas id="view" width="600" height="340"></canvas>
<canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');
function showMessage(text) { // 2D fallback when WebGPU is missing
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}
// In Node.js the first lines would be: import { create, globals } from 'webgpu';
// Object.assign(globalThis, globals); const adapter = await create([]).requestAdapter();
// In Deno (--unstable-webgpu), navigator.gpu is global, exactly as in this page.
const shader = /* wgsl */ `
@group(0) @binding(0) var<storage, read_write> price: array<f32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x < arrayLength(&price)) { price[id.x] = round(price[id.x] * 85.0) / 100.0; }
}`;
async function main() {
const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) return showMessage('WebGPU is not available in this browser');
const device = await adapter.requestDevice();
const prices = new Float32Array([14.99, 39.5, 24, 18.75, 16.2, 21.3]); // BookNest's six
const module = device.createShaderModule({ code: shader });
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module } });
const { STORAGE, COPY_SRC, COPY_DST, MAP_READ } = GPUBufferUsage;
const data = device.createBuffer({ size: 24, usage: STORAGE | COPY_SRC | COPY_DST });
const read = device.createBuffer({ size: 24, usage: COPY_DST | MAP_READ });
device.queue.writeBuffer(data, 0, prices);
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: data } }] }));
pass.dispatchWorkgroups(1), pass.end();
encoder.copyBufferToBuffer(data, 0, read, 0, 24);
device.queue.submit([encoder.finish()]);
await read.mapAsync(GPUMapMode.READ);
const sale = [...new Float32Array(read.getMappedRange())];
read.unmap();
// A page has a canvas, so draw what a server script would only print: list price vs 15% off.
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const bars = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage> before: array<f32>;
@group(0) @binding(1) var<storage> after: array<f32>;
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec3f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
let book = i / 2; let isSale = i % 2 == 1;
let w = select(before[book], after[book], isSale) / 40 * 1.1;
let y = 0.62 - f32(book) * 0.25 - select(0.0, 0.09, isSale);
return Out(vec4f(-0.45 + q.x * w, y + q.y * 0.08, 0, 1), select(vec3f(0.72, 0.70, 0.66), vec3f(0.16, 0.56, 0.30), isSale));
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }` });
const render = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module: bars }, fragment: { module: bars, targets: [{ format }] } });
const original = device.createBuffer({ size: 24, usage: STORAGE | COPY_DST });
device.queue.writeBuffer(original, 0, prices);
const out = device.createCommandEncoder();
const draw = out.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
draw.setPipeline(render);
draw.setBindGroup(0, device.createBindGroup({ layout: render.getBindGroupLayout(0), entries: [
{ binding: 0, resource: { buffer: original } }, { binding: 1, resource: { buffer: data } }] }));
draw.draw(4, 12);
draw.end();
device.queue.submit([out.finish()]);
const titles = ['The Quiet Harbor', 'Small Steps', 'Salt and Saffron', "Clockmaker's", 'Night Trains', 'Paper Gardens'];
ink.font = '12px system-ui, sans-serif';
titles.forEach((t, i) => {
const y = (1 - 0.62 + i * 0.25) * 170;
ink.fillStyle = '#222'; ink.textAlign = 'right'; ink.fillText(t, 156, y + 2);
ink.textAlign = 'left'; ink.fillStyle = '#1e6b3a';
ink.fillText(`$${prices[i].toFixed(2)} -> $${sale[i].toFixed(2)}`, 170 + (sale[i] / 40 * 1.1) * 300 + 8, y + 12);
});
ink.font = '11.5px ui-monospace, monospace'; ink.fillStyle = '#555';
ink.fillText(`15% off: [${sale.map((p) => p.toFixed(2)).join(', ')}] (the script's console output)`, 12, 316);
ink.font = '11.5px system-ui, sans-serif';
ink.fillText("Node.js: npm install webgpu (Dawn as an add-on). Deno: wgpu behind --unstable-webgpu.", 12, 334);
}
main();
</script>