WebGPU is finally shipping in all major browsers, and it changes what front-end developers can do with graphics: compute shaders, lower driver overhead, and a modern API designed for the web rather than adapted from OpenGL. But raw WebGPU code is verbose and imperative, which sits awkwardly inside component-based frameworks like React, Vue, and Svelte. The practical solution emerging across the ecosystem is the WebGPU shader component: a reusable component that owns a canvas, compiles WGSL shaders, manages the GPU device, and exposes a declarative API to the surrounding framework. This guide explains how to structure such components, what changes between frameworks, and which pitfalls to avoid in production.
Why WebGPU Changes the Component Model
WebGPU differs from WebGL in ways that directly affect how you wrap it in a component. A WebGL context is tied to a single canvas and largely self-contained; you can initialize it inside a component's mount hook and mostly forget about it. WebGPU, by contrast, is built around long-lived, shareable resources: a single GPUDevice can feed multiple canvases, pipelines can be created once and reused, and WGSL shaders are compiled into shader modules that multiple pipelines can reference.
This has two consequences for component design. First, the device should generally live outside the component — at the application or context level — because creating a device per component wastes the very resource sharing that makes WebGPU efficient. Second, shader modules and pipelines are expensive to compile and should be cached, not rebuilt on every render.
The typical architecture looks like this:
- A device provider at the app root requests an adapter and device asynchronously.
- Shader components declare their WGSL source (or import it) and receive the shared device through context, props, or a store.
- Each component owns only its canvas, its pipeline, and its per-frame draw logic.
If you are new to the API itself, the official WebGPU samples repository and the WGSL specification are the best references for the underlying primitives used below.
A Framework-Agnostic Core
The smartest way to support React, Vue, and Svelte without tripling your work is to keep the GPU logic in plain TypeScript and let each framework adapter be thin. A minimal core might look like this:
// gpu-core.ts
export async function createDevice(): Promise<GPUDevice> {
if (!navigator.gpu) throw new Error("WebGPU not supported");
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("No suitable GPUAdapter");
return adapter.requestDevice();
}
export function createRenderPipeline(
device: GPUDevice,
shaderCode: string,
format: GPUTextureFormat
): GPURenderPipeline {
const module = device.createShaderModule({ code: shaderCode });
return device.createRenderPipeline({
layout: "auto",
vertex: {
module,
entryPoint: "vs_main",
targets: [{ format }],
},
fragment: {
module,
entryPoint: "fs_main",
},
primitive: { topology: "triangle-list" },
});
}
Each framework wrapper then handles only lifecycle: acquiring the canvas, configuring the context, running the render loop, and cleaning up. The WGSL itself stays identical across frameworks — a full-screen gradient shader, for example:
struct VertexOut {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
};
@vertex
fn vs_main(@builtin(vertex_index) i: u32) -> VertexOut {
var pos = array<vec2f, 3>(
vec2f(-1.0, -3.0), vec2f(3.0, 1.0), vec2f(-1.0, 1.0)
);
var out: VertexOut;
out.position = vec4f(pos[i], 0.0, 1.0);
out.uv = pos[i] * 0.5 + 0.5;
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4f {
return vec4f(in.uv, 0.5 + 0.5 * sin(in.uv.x * 10.0), 1.0);
}
Wiring It Into React
In React, the natural fit is a component that receives the device via context and uses useRef for the canvas and useEffect for the render loop. The key discipline is separating setup from per-frame work: pipeline creation belongs in a useEffect with a dependency on the device and shader source, while the animation frame loop reads props through refs to avoid tearing down the loop on every re-render.
A common React mistake is putting reactive state directly into the render loop's dependency array. If a component takes a speed prop and you include it in useEffect dependencies, every prop change destroys and recreates the GPU pipeline — an expensive operation. Instead, store mutable values in a ref, update the ref during render, and let the loop read the ref. For uniform buffers, write new values with device.queue.writeBuffer() rather than recreating buffers.
React 19's improved ref handling and the general move toward concurrent rendering make this pattern even more important: your render loop must be safe against components mounting and unmounting unpredictably. Cancel animation frames and destroy bind groups' underlying textures in the effect cleanup function, but never call device.destroy() from a component — the device is shared.
Wiring It Into Vue and Svelte
Vue maps naturally to the same architecture using composables. A useWebGPU(deviceRef, canvasRef) composable can encapsulate context configuration and the render loop, with watchEffect handling setup and onUnmounted handling teardown. Vue's reactivity system makes it tempting to make shader uniforms deeply reactive; resist that. GPU uploads should be explicit — watch the specific reactive values you care about and write them to the buffer in a watcher callback, not through a reactive proxy on every frame.
Svelte is arguably the cleanest of the three because Svelte 5's runes ($state, $effect, $props) give you fine-grained reactivity without a virtual DOM. A Svelte shader component is typically a .svelte file with a canvas element, an $effect that builds the pipeline when the device resolves, and direct reads of reactive props inside the frame loop. Because Svelte updates are granular, you can often read props directly in the loop without the ref-indirection dance React requires.
In both frameworks, the same rule applies as in React: the render loop should never be torn down because a prop changed. Only the canvas, device, or shader source should trigger re-initialization.
Production Considerations and Common Mistakes
Several issues show up repeatedly when teams ship WebGPU components:
Device loss is a real event, not a theoretical one. A GPUDevice can be lost due to driver resets or system GPU management. Listen for the device's lost promise and re-acquire the device, rebuilding pipelines. Components that assume an immortal device will show a black canvas after a tab restore or driver update.
Canvas configuration must match the pipeline format. Call canvas.getContext("webgpu"), configure it with device and the preferred format from navigator.gpu.getPreferredCanvasFormat(), and use that same format as the pipeline's render target. Mismatched formats are one of the most common first-run errors.
Handle resize correctly. Canvas size changes require reconfiguring the context and recreating the depth buffer if you use one. Use ResizeObserver rather than window resize events, and guard against zero-sized canvases, which produce validation errors.
Know when not to use WebGPU. If your visualization is a few thousand static shapes, SVG or Canvas 2D will outperform a GPU pipeline once you factor in complexity. WebGPU shader components earn their keep with particle systems, image processing, audio visualizers, simulations using compute shaders, and anything with tens of thousands of dynamic elements. Also remember that WebGPU requires a secure context and is unavailable in older browsers, so provide a fallback or a clear capability check.
Bundle WGSL as strings or imports, not inline templates. Keeping shaders in separate .wgsl files imported at build time gives you syntax highlighting and keeps components readable. Most bundlers handle text imports with a small plugin or a ?raw suffix.
Frequently Asked Questions
Yes, and you generally should. The device is framework-agnostic — it is just a JavaScript object — so a single device created at the application level can serve components written in any framework, including mixed micro-frontend setups. Just ensure lifecycle ownership is clear: the app owns the device, components own only their canvases and pipelines.
No. For custom shaders and simple rendering, raw WebGPU with a thin framework wrapper is often simpler and lighter. Libraries such as Three.js are adding WebGPU backends and make sense when you need scenes, loaders, and lighting. For a single shader-driven component, direct WebGPU keeps your bundle small.
Use a uniform buffer. Allocate it once, then each frame call `device.queue.writeBuffer()` with the updated values before encoding the draw command. Do not recreate buffers per frame, and do not route uniform data through framework reactivity in a way that triggers component re-renders — the render loop should read from a mutable ref, store, or plain object.
`navigator.gpu` will be undefined, so check for it before requesting an adapter. Provide a graceful fallback — a static image, a Canvas 2D version, or a message — depending on how central the effect is. Since WebGPU requires a secure context, also verify `window.isSecureContext` when debugging over plain HTTP.
Completely. WGSL is plain text compiled by the GPU driver through the WebGPU API; it knows nothing about your framework. Keeping shaders in shared `.wgsl` files and importing them into each framework wrapper is the recommended approach and avoids divergence between implementations.
Conclusion
Building WebGPU shader components for React, Vue, and Svelte is less about three different integrations and more about one solid core with three thin adapters. Keep the device shared and long-lived, cache pipelines, keep the render loop independent of framework reactivity, and treat device loss and resizing as first-class concerns. The result is GPU-powered visuals that behave like ordinary components: composable, reusable, and framework-idiomatic on the surface. A good next step is to extract the core module from one of your existing WebGL or Canvas experiments, port a single fragment shader to WGSL, and wrap it in your framework of choice — the patterns above will carry you from a gradient demo to production-grade visual components.
Transparency Note: Links in this article are not affiliate links. I recommend these tools and platforms based on their relevance to the topic, and no compensation is received for including them.