use.gpu (gitlab.com/unconed/use.gpu (https://gitlab.com/unconed/use.gpu 377 ), MIT, 0.20.0 on npm 2,036 as @use-gpu/* packages) is Steven Wittens's "declarative, reactive WebGPU legos". You compose components (a plot, axes, lines, points, text, a render-to-texture pass) in a tree, as in React 7,897 , and use.gpu's own React-like runtime, called Live, re-runs only the parts whose inputs changed, so data updates do not rebuild the whole scene. A built-in shader linker composes WGSL snippets and generates the bindings between them, which is the tedious part of raw WebGPU.
Its README calls it "in the alpha stage" and warns not to "expect production-quality code or docs", and the project lives on GitLab 377 with a small community. It was not run here, since its examples are built with its own toolchain rather than loaded from a CDN. Read it for ideas on shader composition and incremental rendering, and prototype GPU-heavy data visualizations with it; use D3 18,089 (D3.js) or an engine for production work.
<!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);
}
// Not use.gpu itself (it needs its own toolchain), but its model: components with inputs,
// re-run only when their inputs change, each owning a GPU buffer. Here in raw WebGPU.
const code = /* wgsl */ `
struct Item { rect: vec4f, color: vec4f }
@group(0) @binding(0) var<storage> items: array<Item>;
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 px = items[i].rect.xy + q * items[i].rect.zw;
return Out(vec4f(px.x / 300 - 1, 1 - px.y / 170, 0, 1), items[i].color.rgb);
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }`;
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 context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: 'auto', primitive: { topology: 'triangle-strip' },
vertex: { module }, fragment: { module, targets: [{ format }] } });
// A component: a render function of its props, a buffer, and a memo of the last props.
function component(name, render, capacity) {
const buffer = device.createBuffer({ size: capacity * 32, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer } }] });
return { name, render, buffer, group, count: 0, last: undefined, runs: 0, flash: 0,
update(props) { // re-run only if the inputs changed
const key = JSON.stringify(props);
if (key === this.last) return;
this.last = key; this.runs++; this.flash = 30;
const items = this.render(props);
device.queue.writeBuffer(buffer, 0, new Float32Array(items.flat()));
this.count = items.length;
} };
}
const X0 = 250, Y0 = 60, W = 330, H = 220;
const axes = component('Axes', () => [[X0, Y0 + H, W, 2, 0.3, 0.3, 0.3, 1], [X0, Y0, 2, H, 0.3, 0.3, 0.3, 1],
...[1, 2, 3, 4].map((k) => [X0, Y0 + H - k * H / 5, W, 1, 0.85, 0.84, 0.8, 1])], 8);
const bars = component('Bars', ({ sales }) => sales.map((s, i) => [X0 + 12 + i * 52, Y0 + H - s * 4, 36, s * 4, 0.08, 0.40, 0.75, 1]), 8);
const highlight = component('Highlight', ({ sales }) => {
const best = sales.indexOf(Math.max(...sales));
return [[X0 + 8 + best * 52, Y0 + H - sales[best] * 4 - 10, 44, 6, 0.9, 0.6, 0.15, 1]];
}, 1);
const tree = [['Plot', 0, null], ['Axes', 1, axes], ['Bars', 1, bars], ['Highlight', 1, highlight]];
let sales = [22, 35, 18, 41, 27, 30], tick = -1;
function frame(now) {
const t = Math.floor(now / 1200);
if (t !== tick) { // new data: one book's sales change
tick = t;
if (t > 0) { sales = [...sales]; sales[t % 6] = 12 + ((t * 17) % 38); }
}
axes.update({}); // no inputs: runs once, never again
bars.update({ sales });
highlight.update({ sales: sales.slice() }); // same data, but only re-runs when sales change
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
pass.setPipeline(pipeline);
for (const c of [axes, bars, highlight]) { pass.setBindGroup(0, c.group); pass.draw(4, c.count); }
pass.end();
device.queue.submit([encoder.finish()]);
ink.clearRect(0, 0, 600, 340);
ink.font = 'bold 13px system-ui, sans-serif'; ink.fillStyle = '#222';
ink.fillText('Component tree', 16, 30);
ink.fillText('Weekly sales (bars rebuilt only when data changes)', 250, 30);
tree.forEach(([name, depth, c], i) => {
const x = 20 + depth * 30, y = 50 + i * 56;
const hot = c && c.flash > 0;
if (c && c.flash > 0) c.flash--;
ink.fillStyle = hot ? '#f6c55b' : '#ffffff'; ink.beginPath(); ink.roundRect(x, y, 170 - depth * 30, 40, 8); ink.fill();
ink.strokeStyle = '#999'; ink.stroke();
ink.fillStyle = '#222'; ink.font = 'bold 12px system-ui, sans-serif'; ink.fillText(`<${name}>`, x + 10, y + 17);
ink.font = '11px system-ui, sans-serif'; ink.fillStyle = '#555';
ink.fillText(c ? `re-ran ${c.runs}x, buffer ${c.count * 32} B` : 'container', x + 10, y + 32);
});
ink.fillStyle = '#555'; ink.font = '11.5px system-ui, sans-serif';
ink.fillText('use.gpu (MIT, 0.20.0, alpha) runs this model with its own React-like runtime, Live, and a WGSL linker.', 16, 330);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
</script>