WebGPU in Node.js and Deno

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:

demos/ch04/node/discount.mjs: a compute shader in Node.js through DawnJavaScript
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();
Output
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().

The discount compute shader from the Node.js script, run here in the browser: the same WGSL and API calls, prices drawn before and afterHTMLLive
<!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>