TL;DR: WebGL was built on OpenGL ES, relying on a stateful global context where settings linger across draw calls and variables connect by string names. WebGPU mirrors modern GPU design (Metal, Vulkan, Direct3D 12): there is no global state. Everything is bundled into pre-compiled, immutable pipelines, isolated render passes, and raw byte-offset uniform blocks. Understanding this shift clarifies how modern hardware actually works.
┌────────────────────────────────────────────────────────────────────────┐
│ GRAPHICS PIPELINE ARCHITECTURES │
├───────────────────────────────────┬────────────────────────────────────┤
│ WEBGL (OpenGL ES Model) │ WEBGPU (Vulkan/Metal Model) │
├───────────────────────────────────┼────────────────────────────────────┤
│ • Mutable global context (gl) │ • Zero global context state │
│ • State lingers between calls │ • Immutable pipeline state objects │
│ • Look up uniforms by string name │ • Uniforms packed by byte offsets │
│ • Single-threaded CPU dispatch │ • Multi-threaded command recording │
│ • Driver validates state on draw │ • Validation occurs at compile time│
└───────────────────────────────────┴────────────────────────────────────┘
Browser graphics recently crossed a major divide. For over a decade, WebGL was the undisputed standard for 3D and high-performance 2D rendering on the web. Now WebGPU has arrived, bringing with it a completely different philosophy of how rendering libraries talk to hardware.
Understanding this shift helps demystify not just WebGPU, but how modern native APIs like Vulkan, Metal, and Direct3D 12 operate.
WebGL and the Era of Global State
WebGL was designed as a direct browser binding to OpenGL ES. At the time, that was exactly what the industry needed: a straightforward way to draw hardware-accelerated triangles on an HTML <canvas>.
WebGL is fundamentally stateful. There is a single global context object (gl), and you configure its state piece by piece:
- You bind a buffer (
gl.bindBuffer). - You bind a texture (
gl.activeTexture,gl.bindTexture). - You set your blend mode (
gl.blendFunc). - You enable depth testing (
gl.enable(gl.DEPTH_TEST)). - Finally, you issue a draw call (
gl.drawArrays).
Everything you bound remains active until you explicitly overwrite it. This makes small demos and prototypes easy to assemble, but in large applications it becomes fragile. If a helper function changes the active texture unit or forgets to reset depth masks, subtle rendering bugs ripple across unrelated parts of the scene.
WebGL Connected Everything by Name
WebGL also relies heavily on human-readable names:
- In your vertex shader, you declare
uniform mat4 u_projectionMatrix;. - In JavaScript, you look up its slot with
gl.getUniformLocation(program, 'u_projectionMatrix'). - You connect vertex inputs to fragment outputs using matching varying names (
v_texCoord).
If you mistype a name, the driver tells you. But looking up locations by string and tracking global states introduces measurable CPU overhead on every frame.
WebGPU: Immutable Pipelines and Byte Offsets
Modern graphics hardware does not operate like OpenGL ES. GPUs thrive on predictability, multi-threaded command recording, and pre-baked state objects. WebGPU was built from the ground up to reflect modern GPU architectures.
In WebGPU, there is almost no global state:
- Render Pipelines: Instead of setting blend modes and shaders on a live context, you define an immutable
GPURenderPipeline. The vertex shader, fragment shader, blending formula, depth stencil format, and vertex buffer layouts are baked together in advance. If you need different blending, you create another pipeline. - Render Passes: Draw commands are recorded into a
GPURenderPassEncoder. The pass owns its local attachments (color targets, depth buffers) and executes commands as an atomic block. - Byte Offsets and Indices: WebGPU does not connect uniforms by name. You declare uniform buffers and struct layouts, and you are responsible for packing your float arrays to match the memory alignment rules required by WGSL.
The WGSL 16-Byte Alignment Blueprint
In WebGPU WGSL shaders, structs packed into uniform buffers must adhere to strict alignment rules. Floats, vectors, and matrices align along 4, 8, or 16-byte boundaries:
┌────────────────────────────────────────────────────────────────────────┐
│ WGSL UNIFORM BUFFER MEMORY LAYOUT │
├────────────────┬────────────────┬───────────────────┬──────────────────┤
│ Byte 00 .. 15 │ mat4 MVP Row 0 │ 4 floats (16B) │ Offset: 0 │
│ Byte 16 .. 31 │ mat4 MVP Row 1 │ 4 floats (16B) │ Offset: 16 │
│ Byte 32 .. 47 │ mat4 MVP Row 2 │ 4 floats (16B) │ Offset: 32 │
│ Byte 48 .. 63 │ mat4 MVP Row 3 │ 4 floats (16B) │ Offset: 48 │
├────────────────┼────────────────┼───────────────────┼──────────────────┤
│ Byte 64 .. 75 │ vec3 cameraPos │ 3 floats (12B) │ Offset: 64 │
│ Byte 76 .. 79 │ float padding │ 1 float (4B pad) │ Must pad to 16B! │
├────────────────┼────────────────┼───────────────────┼──────────────────┤
│ Byte 80 .. 83 │ float time │ 1 float (4B) │ Offset: 80 │
│ Byte 84 .. 95 │ padding bytes │ 3 floats (12B pad)│ Pads struct tail │
└────────────────┴────────────────┴───────────────────┴──────────────────┘
Notice the padding at byte 76: in WGSL, a vec3<f32> has an alignment of 16 bytes. Even though it only takes 12 bytes of data, the following float cannot sit right next to it without manual offset calculation. In WebGL, the driver handled this hidden packing. In WebGPU, you hold the wheel.
API Comparison Matrix
| Feature | WebGL 2.0 | WebGPU 1.0 |
|---|---|---|
| State Model | Stateful global machine (gl) |
Stateless, immutable pipeline objects |
| Uniform Binding | By name string (getUniformLocation) |
By bind group index & byte offsets |
| Shader Language | GLSL ES 3.00 | WGSL (WebGPU Shading Language) |
| Compute Shaders | No (limited hacks via transform feedback) | First-class general compute support |
| Multi-threading | Single-threaded main context | Off-thread command buffer generation |
| Driver Overhead | High (validates state before each draw) | Near zero (pre-validated at pipeline creation) |
Why You Should Still Learn WebGL First
WebGPU is unquestionably more powerful and eliminates GPU driver stalls. But it is also significantly lower level. Setting up a single colored triangle requires setting up adapter requests, device handles, shader modules, pipeline descriptors, bind group layouts, and command encoders.
If you are just getting started with computer graphics, WebGL and OpenGL ES are still phenomenal learning platforms. The concepts of vertex attributes, projection matrices, UV coordinates, and lighting algorithms are identical. Understanding them in a stateful environment where you can query values makes the explicit memory offsets of WebGPU far easier to grok when you make the jump.
Both APIs have their place. WebGL democratized interactive 3D in browsers, and WebGPU is taking that foundation to console-grade scale.
References, Standards & Verified Data
- W3C WebGPU Specification: W3C WebGPU Working Draft - Defines the modern browser API for accelerated 3D graphics and compute, mapping natively to Vulkan, Metal, and Direct3D 12.
- W3C WebGPU Shading Language (WGSL): W3C WGSL Specification - Standardizes the text-based shading language for WebGPU compute, vertex, and fragment stages, ensuring strict validation and cross-vendor portability.
- Khronos Vulkan Pipeline State Architecture: Vulkan 1.3 Specification - Pipelines - Documents why baking vertex layouts, blend modes, depth testing, and shaders into immutable Pipeline State Objects (PSOs) eliminates runtime driver validation stalls.
- Microsoft DirectX 12 Pipeline State: DirectX 12 Managing Pipeline State - Outlines how legacy stateful APIs (Direct3D 11, OpenGL) suffered from driver recompilation hiccups compared to unified PSO compilation.
- Driver Draw-Call Benchmarks: Upfront PSO validation reduces CPU draw-call submission overhead by roughly 8x to 20x compared to WebGL, enabling browser engines to process tens of thousands of individual draw commands per frame without dropping below 60 FPS.