Getting Started with WebGPU

Updated June 2026
This guide walks you through initializing WebGPU in the browser, writing your first WGSL shader, creating a render pipeline, and drawing geometry to a canvas element. Each step builds on the previous one, taking you from an empty HTML page to a working WebGPU rendering loop.

WebGPU's initialization process is more explicit than WebGL's, but every step exists for a reason. You are telling the GPU exactly what resources you need, how your shaders are configured, and what your rendering targets look like. This upfront investment eliminates the per-frame overhead that makes WebGL slow in complex scenes.

Check Browser Support and Request the GPU Device

WebGPU availability is exposed through the navigator.gpu object. If this object exists, the browser supports WebGPU. The first API call is navigator.gpu.requestAdapter(), which returns a GPUAdapter representing a physical GPU on the system. The adapter exposes information about the GPU's capabilities, including supported features and limits like maximum texture size, maximum buffer size, and maximum bind groups.

From the adapter, you call requestDevice() to get a GPUDevice. The device is your primary interface for creating all WebGPU resources: buffers, textures, shader modules, pipelines, and bind groups. You can optionally request specific features or higher limits when creating the device, though the defaults are sufficient for most introductory projects. The device also provides a GPUQueue through device.queue, which is where you submit command buffers for execution.

If navigator.gpu is undefined, the browser does not support WebGPU. Your application should detect this and either fall back to WebGL or display a message directing the user to a supported browser. All current versions of Chrome, Edge, Firefox (desktop), and Safari support WebGPU as of mid-2026.

Configure the Canvas for WebGPU Rendering

WebGPU renders to an HTML canvas element, just like WebGL. You get a GPUCanvasContext by calling canvas.getContext("webgpu") on a standard canvas element. This context manages the swap chain, the series of textures that WebGPU renders to and the browser composites into the page.

You configure the canvas context with context.configure(), passing the device and a texture format. The preferred format is obtained from navigator.gpu.getPreferredCanvasFormat(), which returns the format most efficient for the current platform (typically "bgra8unorm" on most systems, "rgba8unorm" on some). Using the preferred format avoids format conversion overhead when the rendered texture is composited to the screen.

The canvas dimensions determine the rendering resolution. For sharp rendering on high-DPI displays, set the canvas width and height attributes to the element's CSS pixel size multiplied by window.devicePixelRatio. This gives you a 1:1 pixel mapping on retina and high-DPI screens.

Write a WGSL Shader Module

A minimal WebGPU shader needs two entry points: a vertex shader and a fragment shader. The vertex shader transforms input positions into clip space (the coordinate system the GPU uses for rasterization). The fragment shader outputs a color for each pixel.

In WGSL, the vertex shader entry point is marked with @vertex and returns a struct or built-in value with @builtin(position). A simple vertex shader reads positions from a vertex buffer using @location(0) and returns them as vec4f values. The fragment shader entry point is marked with @fragment and returns a vec4f color value with @location(0) targeting the first color attachment.

You compile the shader source into a GPUShaderModule by calling device.createShaderModule() with a code property containing the WGSL source as a string. The browser validates the shader immediately. If the source contains syntax errors, type mismatches, or invalid constructs, the module creation fails with a descriptive error message. This immediate validation is one of WGSL's major advantages over GLSL, where errors often surfaced only at draw time.

Create the Render Pipeline

The render pipeline combines your shader module with all the rendering state needed to draw geometry. You create it with device.createRenderPipeline(), passing a descriptor that specifies the vertex entry point, fragment entry point, vertex buffer layouts, primitive topology, and color target format.

The vertex state includes a buffers array describing the layout of each vertex buffer. Each buffer layout specifies the stride (bytes between vertices) and an array of attributes with their format (float32x2 for 2D positions, float32x3 for 3D positions, float32x4 for colors), offset within the vertex, and shader location number that maps to the @location attribute in WGSL.

The fragment state includes a targets array describing each color attachment. For a simple case, you have one target with the canvas format and no blending. The primitive state defaults to triangle-list topology, meaning every three vertices form one triangle. You can also use triangle-strip, line-list, line-strip, and point-list topologies.

For production applications, use device.createRenderPipelineAsync() instead of the synchronous version. The async variant returns a promise that resolves when the GPU has finished compiling the pipeline, preventing the main thread from stalling during shader compilation.

Create Vertex Buffers and Upload Geometry

Vertex data is stored in GPUBuffer objects created with the GPUBufferUsage.VERTEX flag. You specify the buffer's size in bytes and a usage bitmask that tells the GPU how the buffer will be used. For a vertex buffer that receives data from the CPU, you need GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST.

Write data to the buffer using device.queue.writeBuffer(), which copies a JavaScript ArrayBuffer or TypedArray into GPU memory. For a triangle with three 2D vertices, you create a Float32Array with six values (x1, y1, x2, y2, x3, y3) and write it to the buffer. The buffer size must be at least as large as the data being written.

The data format must match the vertex attribute format declared in the pipeline. If the pipeline specifies float32x2 for position, each vertex consumes 8 bytes (two 32-bit floats). If you add a float32x4 color attribute, each vertex consumes 24 bytes (two position floats plus four color floats), and the stride in the pipeline's buffer layout must reflect this total.

Record and Submit the Render Pass

Each frame, you get the current texture from the canvas context with context.getCurrentTexture(), then create a texture view from it. This view becomes the color attachment for your render pass. You create a GPUCommandEncoder with device.createCommandEncoder(), which is the object that records GPU commands.

Begin a render pass with encoder.beginRenderPass(), passing a descriptor that specifies color attachments. Each color attachment includes the texture view, a load operation ("clear" to fill with a background color, "load" to preserve existing content), a store operation ("store" to write results, "discard" to throw them away), and a clear color if loading with "clear".

Inside the render pass, bind your pipeline with pass.setPipeline(), bind your vertex buffer with pass.setVertexBuffer(0, vertexBuffer), and issue a draw call with pass.draw(vertexCount). The vertex count tells the GPU how many vertices to process. End the pass with pass.end().

Finalize the command encoder into a command buffer with encoder.finish(), then submit it with device.queue.submit([commandBuffer]). The GPU processes the commands asynchronously, and the rendered result appears on the canvas after the browser composites the next frame.

Build a Render Loop with requestAnimationFrame

A static image requires only one render pass submission, but games need continuous rendering. Wrap your rendering code in a function and call it with requestAnimationFrame() to create a loop that renders a new frame every time the browser is ready to paint. The browser calls your function at the display's refresh rate, typically 60 Hz or 120 Hz on newer monitors.

Each frame, you must get a new texture from the canvas context because the swap chain cycles through textures. The command encoder and command buffer are also created fresh each frame because they cannot be reused after submission. Pipeline and vertex buffer objects persist across frames, you do not recreate them.

To animate geometry, update the data in your vertex buffers or uniform buffers before recording the render pass. For camera movement, update a uniform buffer containing the view and projection matrices. For animated objects, update a uniform buffer containing the model matrix. The pattern of update-record-submit repeats every frame, with the CPU and GPU working in parallel on different frames.

Key Takeaway

WebGPU initialization is more verbose than WebGL because you explicitly configure every aspect of the rendering pipeline upfront. This investment pays off immediately with zero per-draw validation overhead, predictable performance, and clear error messages when something is misconfigured.