First steps beyond WebGL
WebGPU is the web's modern answer to direct GPU programming and is gradually replacing WebGL as the standard for heavy graphics and computation work in the browser. Once you understand the core concepts, you gain access to compute shaders, more efficient memory management, and a much more modern API.
Table of Contents
- 1. Why WebGL hits its limits
- 2. GPUDevice as the central entry point
- 3. Command buffers and explicit command creation
- 4. Writing shaders in WGSL
- 5. Compute shaders for work beyond graphics
- 6. Buffers, bind groups, and explicit memory management
- 7. When switching from WebGL pays off
- 8. Current state of browser support
- 9. A minimal, complete triangle
- 10. Summary
- 11. FAQ
1. Why WebGL hits its limits
WebGL is based on OpenGL ES, an API designed for the graphics hardware and programming models of the early 2010s. Modern GPUs today work with quite different concepts such as explicit command buffers, pipeline objects, and parallel command generation, which WebGL can only map awkwardly or not at all.
In practice this shows up as CPU overhead: WebGL calls like gl.drawArrays() trigger a lot of internal validation and state management work in the driver, which becomes a bottleneck in complex scenes with many draw calls. WebGPU was designed from the ground up for modern graphics APIs like Vulkan, Metal, and Direct3D 12, and adopts their more efficient underlying principles.
2. GPUDevice as the central entry point
Getting started with WebGPU begins with requesting a GPUAdapter via navigator.gpu.requestAdapter(), which represents the available GPU hardware. From this adapter you can then request a GPUDevice, the actual interface through which all further operations run. Both the adapter and device requests are asynchronous and can be parameterized with optional feature and limit requests, so an application specifically asks only for the capabilities it actually needs.
The device encapsulates resources such as buffers, textures, and pipelines, and provides queues through which commands are sent to the GPU. Unlike WebGL, where a global context implicitly holds state, in WebGPU every resource is explicitly created and referenced, which reduces sources of error and makes browser-side optimizations easier.
async function initWebGPU() {
if (!navigator.gpu) {
throw new Error('WebGPU is not supported by this browser');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('No suitable GPU adapter found');
}
const device = await adapter.requestDevice();
const canvas = document.querySelector('#gpu-canvas');
const context = canvas.getContext('webgpu');
context.configure({
device,
format: navigator.gpu.getPreferredCanvasFormat(),
});
return { device, context };
}
3. Command buffers and explicit command creation
Instead of sending commands to the GPU individually and immediately, WebGPU collects them in a GPUCommandEncoder, which records drawing, compute, and copy operations. At the end, a GPUCommandBuffer is created from this and submitted for execution as a batch via a GPUQueue.
This model lets the application prepare commands in parallel across multiple CPU threads before submitting them collectively, a pattern borrowed from native graphics programming that simply did not exist in WebGL. For typical web applications this mainly means: less CPU time per frame and more predictability in complex scenes. Even when a given application does not actively use multiple threads for command creation, it still benefits from the batching principle alone through fewer individual driver calls per frame.
function renderFrame(device, context, pipeline) {
const encoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
view: textureView,
clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
renderPass.setPipeline(pipeline);
renderPass.draw(3);
renderPass.end();
device.queue.submit([encoder.finish()]);
}
4. Writing shaders in WGSL
WebGPU uses WGSL, the WebGPU Shading Language, its own text-based language for vertex, fragment, and compute shaders, instead of the GLSL known from WebGL. WGSL was specifically designed for safety and straightforward translation into the shader languages of the underlying native APIs, which helps browsers guarantee consistent behavior across platforms.
Syntactically, WGSL resembles a mix of Rust and C, with explicit type annotations and clearly defined input and output structures for each shader stage. Anyone already familiar with GLSL will find their way around quickly, but needs to get used to the stricter type rules and explicit struct declarations. Many tooling providers, including editor extensions for syntax highlighting and linting, now support WGSL as well, which has noticeably eased the learning curve compared to the language's early days.
const shaderCode = `
@vertex
fn vs_main(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
var positions = array<vec2f, 3>(
vec2f( 0.0, 0.5),
vec2f(-0.5, -0.5),
vec2f( 0.5, -0.5)
);
return vec4f(positions[index], 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4f {
return vec4f(0.2, 0.7, 1.0, 1.0);
}
`;
const shaderModule = device.createShaderModule({ code: shaderCode });
5. Compute shaders for work beyond graphics
Besides classic graphics rendering, WebGPU offers full-fledged compute shaders that let you run arbitrary parallel computations directly on the GPU, such as physics simulations, image processing, or even smaller machine learning inference tasks. WebGL practically did not have this capability, workarounds via textures as data storage were required.
A compute shader is written in WGSL similarly to a graphics shader, but operates on work groups of parallel threads declared via @compute @workgroup_size(...). Results land in storage buffers, which can then be read back to the CPU or reused directly for further rendering.
6. Buffers, bind groups, and explicit memory management
Unlike WebGL, where uniforms and attributes are addressed through implicit locations, WebGPU manages memory resources via explicit GPUBuffer objects and GPUBindGroup structures that precisely define which resource is available at which binding slot in the shader. A buffer is created with a fixed size and a usage flag such as GPUBufferUsage.VERTEX or GPUBufferUsage.STORAGE, so the browser and ultimately the driver know from the start how the resource should ideally be placed in memory.
This explicitness initially feels like more boilerplate compared to the more permissive WebGL model, but it pays off in larger applications: bind groups can be created ahead of time and reused across frames instead of resetting state on every draw call. For teams coming from WebGL, this step is usually the biggest conceptual shift, since it requires defining resource layout and binding structure already at pipeline design time instead of assembling it casually at runtime.
7. When switching from WebGL pays off
For simple 2D visualizations, small 3D scenes, or projects with a limited time budget, WebGL often remains the more pragmatic choice, since the toolchain is more mature and considerably more libraries, tutorials, and ready-made examples exist. WebGPU pays off especially where CPU overhead from many draw calls becomes a real problem or where compute shaders are needed.
Projects that lean heavily on GPU-based physics, particle simulations with hundreds of thousands of elements, or custom rendering pipelines using modern techniques like deferred shading also benefit considerably more from WebGPU than from WebGL. The decision should hinge on actual bottleneck analysis, not mere curiosity about the new API. A pragmatic middle ground is to leave existing WebGL applications unchanged for now and use WebGPU specifically for individual new feature areas, for example a newly added particle simulation, while the rest of the application stays untouched.
8. Current state of browser support
Chrome and Edge now support WebGPU by default on desktop and increasingly on Android, Firefox offers support behind flags or already stable in recent versions, Safari is catching up with its own implementation progress. Support is growing noticeably faster than WebGL's did in its early years.
For production applications, feature detection via if ('gpu' in navigator) combined with a WebGL fallback path is still recommended, especially when the target audience includes older devices or browsers. Libraries like Three.js now offer experimental WebGPU renderers alongside their established WebGL renderer. Caution is also warranted on mobile devices: even when a GPUAdapter can be requested successfully, limits such as maximum buffer size or the number of simultaneous bind groups can differ noticeably between desktop and mobile GPUs, which is why a look at adapter.limits is worthwhile before production use.
9. A minimal, complete triangle
To make the concepts tangible, here is a complete, if minimal, example that assembles the previously described building blocks, device, shader module, render pipeline, and command buffer, into a working triangle. This pattern is the classic entry point in practically every WebGPU tutorial and forms the foundation for more complex scenes.
The table below summarizes the most important conceptual differences between WebGL and WebGPU and helps assess which API is the better foundation for a specific project.
async function createTrianglePipeline(device, format) {
const module = device.createShaderModule({ code: shaderCode });
return device.createRenderPipeline({
layout: 'auto',
vertex: { module, entryPoint: 'vs_main' },
fragment: {
module,
entryPoint: 'fs_main',
targets: [{ format }],
},
primitive: { topology: 'triangle-list' },
});
}
const { device, context } = await initWebGPU();
const format = navigator.gpu.getPreferredCanvasFormat();
const pipeline = await createTrianglePipeline(device, format);
renderFrame(device, context, pipeline);
| Aspect | WebGL | WebGPU | Impact |
|---|---|---|---|
| Shader language | GLSL | WGSL | Different syntax, stricter typing |
| Command model | Immediate, state-based calls | Command buffers, batched submission | Less CPU overhead with many draw calls |
| Compute capability | Not supported, texture workarounds | Native compute shaders | Direct GPU computation without detours |
| Browser maturity | Very broad, stable for many years | Growing, Chrome/Edge/Firefox leading | WebGL remains a safe fallback |
Mironsoft
Modern browser APIs, performance, and maintainable JavaScript
JavaScript that holds up in the real browser, not just in the tutorial?
We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.
Code Review
Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.
Performance Optimization
Improving bundle size, load time, and runtime performance with modern APIs.
Modernization
Deliberately introducing native browser APIs instead of heavy libraries.
10. Summary
WebGPU: The Essentials at a Glance
Entry point
Request a GPUAdapter, create a GPUDevice from it, the central starting point for all operations.
Commands
Command encoder collects operations, batched submission via the queue reduces CPU overhead.
Shaders
WGSL replaces GLSL, supports both graphics and compute shaders.
Decision
Switching pays off with many draw calls or real compute needs, otherwise WebGL stays pragmatic.