WebGPU and WGSL typed for TypeScript: adapter and device, canvas setup, buffers, bind groups, render
and compute pipelines, textures, errors and fallbacks. Status checked against MDN browser-compat-data
in September 2026. For the older API see WebGL.
Support & typing
MDN status: Limited availability (not Baseline). Secure contexts only (HTTPS or localhost).
Available in windows and dedicated/shared workers.
Browser
Since
Scope
Chrome, Edge
113
Windows, macOS, ChromeOS; Linux since 144 (Intel Gen12+ GPUs only)
Chrome Android
121
device and GPU dependent
Safari (macOS, iOS, iPadOS, visionOS)
26
Firefox
141
Windows; Apple-silicon macOS since 145 (Tahoe) / 147 (older); not Linux, Intel Macs or Android; not in service workers
Deno
built in
navigator.gpu
Bun, Node
via npm webgpu (Dawn)
no built-in navigator.gpu
Even in a supporting browser, requestAdapter() can resolve null (blocklisted GPU, no hardware
acceleration). Always keep a fallback.
TypeScript
Setup
TS 5.x
bun add -d @webgpu/types, then "types": ["@webgpu/types"] in tsconfig.json
TS 6.0
lib.dom has the GPU* interfaces but not the GPUBufferUsage/GPUTextureUsage/GPUShaderStage/GPUMapMode constants, and types getContext("webgpu") as RenderingContext; keep @webgpu/types (no conflicts)
Worker files
same types; navigator.gpu is on WorkerNavigator
Usage flags
GPUBufferUsage.* etc. are plain numbers OR-ed together
Typed arrays (TS 5.7+)
writeBuffer wants Float32Array<ArrayBuffer>; a bare Float32Array param is ArrayBufferLike and fails
Setting "types" turns off automatic @types/* loading, so list the others too
(["@webgpu/types", "bun"]), or use /// <reference types="@webgpu/types" /> in one file.
Keeping WGSL in files
src/├── gpu/│ ├── shaders/│ │ ├── triangle.wgsl│ │ └── blur.wgsl│ └── renderer.ts # imports the .wgsl files as text└── wgsl.d.ts # types for *.wgsl imports
Bun imports text files with import code from "./shaders/triangle.wgsl" with { type: "text" }; Vite uses
"./x.wgsl?raw". Type them once in wgsl.d.ts: declare module "*.wgsl" { const s: string; export default s }.
Adapter & device
export async function initGPU() { if (!navigator.gpu) throw new Error("WebGPU unsupported"); const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance", }); if (!adapter) throw new Error("No suitable GPU adapter"); const hasF16 = adapter.features.has("shader-f16"); const device = await adapter.requestDevice({ label: "main", requiredFeatures: hasF16 ? ["shader-f16"] : [], requiredLimits: { // ask for more than the default only when needed maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, }, }); return { adapter, device, hasF16 };}
writeBuffer and mapped ranges work in multiples of 4 bytes
Uniform structs
pad the buffer to the WGSL struct size (multiple of 16)
Dynamic offsets
multiples of minUniformBufferOffsetAlignment (256)
mapAsync(GPUMapMode.READ)
resolves after the GPU is done with the buffer
getMappedRange()
ArrayBuffer detached on unmap(): copy with .slice(0) first
mapState
"unmapped", "pending", "mapped"
A mapped buffer
cannot be used in a submit
buffer.destroy()
free now instead of waiting for GC
Bind groups & layouts
A bind group is the set of resources (buffers, textures, samplers) a pipeline reads; its layout
is the matching shape. layout: "auto" derives it from the shader but ties bind groups to one
pipeline; explicit layouts let pipelines share bind groups.
Group by update frequency: @group(0) per frame (camera), @group(1) per material, @group(2) per
object. Switching a low group index is cheapest to keep stable.
device.importExternalTexture({ source: video }) → texture_external; valid for the current task only
Texture origin
texel (0, 0) is top-left; framebuffer y points down, NDC y points up
Comparison sampler
compare: "less" + texture_depth_2d for shadow maps
MSAA
sampleCount: 4 texture as view, canvas texture as resolveTarget
Error handling
Most WebGPU errors are asynchronous: calls don't throw, invalid objects poison whatever uses them,
and the message arrives later. Label objects and listen.
device.lost.then((info) => { console.error(`GPU lost (${info.reason})`, info.message); if (info.reason !== "destroyed") void restartRenderer();});device.onuncapturederror = (e) => { console.error("WebGPU error:", e.error.message);};// Catch errors from a specific block of callsdevice.pushErrorScope("validation");const p = device.createRenderPipeline(desc);const err = await device.popErrorScope();if (err) throw new Error(`Invalid pipeline: ${err.message}`);
// Shader diagnostics with line numbersconst shader = device.createShaderModule({ code });const { messages } = await shader.getCompilationInfo();for (const m of messages) { const where = `${m.lineNum}:${m.linePos}`; if (m.type === "error") console.error(where, m.message);}
Mechanism
Catches
pushErrorScope("validation")
invalid descriptors, bad bindings, wrong usage
pushErrorScope("out-of-memory")
allocation failures
pushErrorScope("internal")
driver or implementation failures
onuncapturederror / uncapturederror event
anything not caught by a scope; the console also logs it
createRenderPipelineAsync()
rejects with GPUPipelineError (reason: validation, internal)
device.lost
resolves once: reason"destroyed" (you called destroy) or "unknown" (driver reset, GPU process crash)
After a loss, request a new adapter and device and recreate every resource; keep CPU-side sources so
this is possible.
Pitfalls
Pitfall
Symptom
Fix
Plain HTTP (not localhost)
navigator.gpu undefined
serve over HTTPS
Caching getCurrentTexture()
"destroyed texture" errors
fetch it every frame
Depth texture not resized
attachment size mismatch
recreate it when the canvas size changes
vec3f in a uniform struct
fields shifted
it aligns to 16; use vec4f or pad
Uniform buffer smaller than the struct
binding validation error
round size up to 16
Reading mapped data after unmap()
detached ArrayBuffer
getMappedRange().slice(0)
textureSample inside a non-uniform if
shader compile error
sample first, or textureSampleLevel
rgba32float with a filtering sampler
layout error
"unfilterable-float" + "non-filtering", or the float32-filterable feature
Passing a GPUBuffer or GPUTexture straight as resource/view
works in Chromium only
use { buffer } and createView()
Integer / float mixing in WGSL
type error
explicit f32(x), u32(y)
Expecting WebGL NDC
geometry clipped
depth is 0..1; use wgpu-matrix or adjust projection
WebGPU vs WebGL
WebGL2
WebGPU equivalent
GLSL ES 3.00
WGSL
createProgram + global state (enable, blendFunc, depthFunc)
Use as the template for any data-parallel job: upload, dispatch, read back with readBuffer.
await gpuDouble(device, Float32Array.of(1, 2, 3)) gives [2, 4, 6].