Introducing WebGPU-Bench: A WebGPU Microbenchmark

Ben Houston's Website ·

9 min read Original article ↗

I want developers and AI agents to be able to write performant WebGPU code. To do that, they need to know what different operations, code patterns, and optional features actually cost on the devices people use.

That is why I built webgpu-bench, the micro-benchmark suite now available on Web3D Survey. It runs small, focused GPU workloads to measure arithmetic, memory access, and alternative shader implementations. You can run it on the website or from the command line, and use the results to guide optimization.

Web3D Survey benchmark result showing throughput summaries and device details

What is a Micro-Benchmark?#

A micro-benchmark suite measures small, focused workloads to reveal the cost of specific operations and implementation choices. An application benchmark measures a larger workload, such as rendering a scene or running a model, where many of those costs interact. The micro-benchmarks help you decide what to optimize; the application benchmark tells you whether those changes improve the overall result.

Why I built it#

Web3D Survey already collects information about which graphics APIs and features real browsers support. Benchmarking adds another question: how well do those features perform? Knowing that a device supports half-precision arithmetic tells you that you can use it. Measuring it helps you decide whether doing so is worthwhile for your workload.

The motivation came partly from my work on Three-LLM, which runs language models through Three.js and WebGPU, and neural texture compression, which reconstructs material textures with a small neural decoder inside a shader. Both raised concrete optimization questions. Would half precision improve throughput? How much would a different memory layout help? When would reducing memory traffic justify extra arithmetic?

I wanted measurements I could use to make those decisions across phones, laptops, and desktop GPUs. I also wanted an AI coding agent working on the same problems to have access to that evidence. An agent can propose a shader rewrite, but it needs a way to test the performance assumptions behind it.

What the benchmarks measure#

The suite currently includes 71 benchmarks and takes roughly one minute to run in full, depending on the device and runtime. Tests that require unsupported features are skipped. It measures individual operations and compares compute strategies across four areas:

  • Arithmetic throughput: FP32 and FP16 arithmetic in scalar, vector, and matrix forms, alongside integer arithmetic and packed 8-bit dot products.
  • Memory access: linear reads and writes, gather and scatter patterns with different working-set sizes, and a dependent-read probe.
  • Operation costs: division, trigonometric functions, logarithms, square roots, bitwise operations, and branch patterns.
  • Compute strategies: workgroup sizes, particle data layouts, direct reads versus shared-memory tiling, reduction strategies, and ways of aggregating atomic updates.

The compute strategy benchmarks compare implementations of the same fixed task. Within each comparison, the variants produce the same logical result and count the same useful work. That makes the measurements relevant to a practical choice: which implementation is faster for this task on this device?

A result is evidence for that particular workload, GPU, and runtime. It can help you choose what to try in an application; you still need to measure the application to see whether the change helps there.

How the suite works#

The shared library, webgpu-bench-core, contains the shader workloads, benchmark catalog, timing harness, and scheduler. It prepares the kernels, skips tests that require unavailable features, calibrates the amount of work, and takes repeated measurements. Benchmarks complete one at a time in catalog order, with idle gaps and cooldown handling to reduce the effect of throttling. Timing prefers GPU timestamp queries where available, with wall-clock checks and a fallback. The headline result is the fastest observed sample, giving an estimate of attainable throughput under the test conditions.

Run it on Web3D Survey#

To try it in a browser, open the Web3D Survey GPU Benchmark page on a WebGPU-capable device and click Run benchmark. Results are saved to a shareable page, and you can browse recent runs from the same starting point. Running it across the devices you care about gives you concrete data to compare.

Web3D Survey’s WebGPU benchmarking interface

For a concrete example, here are the results from my MacBook Air M3, my main development workhorse.

Run it from the command line#

For a terminal workflow, the webgpu-bench CLI runs the same suite through Node.js and Dawn, a native WebGPU implementation:

npx webgpu-bench

It reports the GPU's identity, features, and limits, followed by the per-benchmark results. By default, it also submits the run to Web3D Survey and prints a link to the result page. You can filter the suite and save JSON locally without submitting a report:

npx webgpu-bench --filter 'f16-*' --json --no-report > f16-results.json

That makes the measurements accessible to scripts and AI agents as well as people. An agent can run a relevant subset, read the structured results, and use them to choose an implementation to test. The CLI uses a different runtime from the browser, so browser performance decisions should also be checked in the target browser.

WebGPU-Bench running in the terminal with per-benchmark results

Benchmark reference#

Here are the 71 benchmarks in the current suite. Labels match the benchmark catalog. FP32 and FP16 refer to 32-bit and 16-bit floating point; FMA means fused multiply-add. Strategy comparisons use the same fixed workload within each family.

Benchmark labelWhat it measures
atomic directBuilding a histogram by updating shared global counters directly with atomic operations.
atomic shardedThe same histogram with updates spread across multiple global counter sets, then combined.
atomic workgroupThe same histogram with updates accumulated within each workgroup before merging globally.
branch coherentConditional arithmetic where threads agree within each workgroup but workgroups can differ.
branch divergentConditional arithmetic where neighboring threads choose different branches.
branch noneArithmetic without conditional branches, providing a baseline for the branch comparisons.
branch uniformConditional arithmetic where all threads choose the same branch.
branch vec4 ifThe same conditional choices expressed as separate branches for each vector component.
branch vec4 selectConditional choices across four-component vectors using vector selection.
f32<->f16 convertRound-trip conversion between 32-bit and packed 16-bit floating-point values.
fp16 divDivision throughput using 16-bit floating-point values.
fp16 lnNatural-logarithm throughput using 16-bit floating-point values.
fp16 mat4 FMARepeated 4×4 matrix–vector arithmetic in 16-bit floating point.
fp16 matvec FMARepeated 4×8 matrix–vector arithmetic in 16-bit floating point, with data held in registers.
fp16 powNon-integer power throughput using 16-bit floating-point values.
fp16 rsqrtReciprocal-square-root throughput using 16-bit floating-point values.
fp16 scalar FMAMultiply-add throughput on individual 16-bit floating-point values.
fp16 sin/cosCombined sine and cosine throughput using 16-bit floating-point values.
fp16 sqrtSquare-root throughput using 16-bit floating-point values.
fp16 vec4 FMAMultiply-add throughput on four-component 16-bit floating-point vectors.
fp32 clampConstraining floating-point values to a fixed range with the clamp operation.
fp32 divDivision throughput using 32-bit floating-point values.
fp32 fma() builtinMultiply-add throughput using the explicit fused multiply-add operation.
fp32 lnNatural-logarithm throughput using 32-bit floating-point values.
fp32 mat4 FMARepeated 4×4 matrix–vector arithmetic in 32-bit floating point.
fp32 matvec FMARepeated 4×8 matrix–vector arithmetic in 32-bit floating point, with data held in registers.
fp32 min/maxConstraining floating-point values to the same range with minimum and maximum operations.
fp32 powNon-integer power throughput using 32-bit floating-point values.
fp32 rsqrtReciprocal-square-root throughput using 32-bit floating-point values.
fp32 scalar FMAMultiply-add throughput on individual 32-bit floating-point values.
fp32 selectChoosing between two floating-point values based on a condition.
fp32 sin/cosCombined sine and cosine throughput using 32-bit floating-point values.
fp32 sqrtSquare-root throughput using 32-bit floating-point values.
fp32 vec4 FMAMultiply-add throughput on four-component 32-bit floating-point vectors.
i32 divDivision throughput using signed 32-bit integers.
i32 mat4 multiply-add32-bit integer multiply-add throughput using a 4×4 matrix–vector calculation.
i32 matvec multiply-add32-bit integer multiply-add throughput using a 4×8 matrix–vector calculation with data held in registers.
i32 scalar multiply-add32-bit integer multiply-add throughput using individual values.
i32 vec4 multiply-add32-bit integer multiply-add throughput using four-component vectors.
i32<->f32 convertRound-trip conversion between signed 32-bit integers and 32-bit floating-point values.
int8 dp4aThroughput of packed dot products on signed 8-bit integers.
int8 dp4a matvecMatrix–vector arithmetic using packed, signed 8-bit dot products.
layout aosParticle updates with all fields for each particle stored together: array of structures.
layout aosoaThe same particle updates with fields grouped into blocks of 32 particles.
layout soaThe same particle updates with each field stored in a separate array: structure of arrays.
read dependent chainFollowing a chain of memory reads where each read determines the next address; measures dependent-access cost including loop and dispatch overhead.
read gather 16kbRead throughput for scattered accesses within a 16 KB working set.
read gather 4mbRead throughput for scattered accesses within a 4 MB working set.
read gather 64mbRead throughput for scattered accesses within a 64 MB working set.
read linearRead throughput when neighboring threads access neighboring data in a large buffer.
reduction serialSumming each segment of an array sequentially within one thread.
reduction subgroupThe same segment sums computed with subgroup operations and combined within each workgroup.
reduction workgroupThe same segment sums computed cooperatively with a shared-memory reduction tree.
texture interp (built-in)Texture sampling with interpolation performed by the GPU’s built-in filtering hardware.
texture interp (manual)The same texture scan with neighboring texels fetched and blended explicitly in the shader.
tile directA neighborhood calculation that reads all neighboring values directly from a storage buffer.
tile sharedThe same neighborhood calculation with a tile loaded into shared workgroup memory for reuse.
tile shared paddedThe same shared-memory calculation with padding between rows to test its effect on access efficiency.
u32 byte pack/unpackPacking four bytes into a 32-bit integer and unpacking them again using bit operations.
u32 countOneBitsCounting the bits set to one in a 32-bit integer.
u32 firstLeadingBitFinding the highest set bit in a 32-bit unsigned integer.
u32 reverseBitsReversing the order of bits in a 32-bit integer.
u32 variable shiftShifting 32-bit integer bits left and right by varying amounts.
uint8 dp4aThroughput of packed dot products on unsigned 8-bit integers.
workgroup 128A fixed arithmetic workload using 128 threads per workgroup, for comparison with the other workgroup sizes.
workgroup 256A fixed arithmetic workload using 256 threads per workgroup, for comparison with the other workgroup sizes.
workgroup 64A fixed arithmetic workload using 64 threads per workgroup, for comparison with the other workgroup sizes.
write linearWrite throughput when neighboring threads store data at neighboring locations.
write scatter 16kbWrite throughput for scattered, non-overlapping destinations within a 16 KB working set.
write scatter 4mbWrite throughput for scattered, non-overlapping destinations within a 4 MB working set.
write scatter 64mbWrite throughput for scattered, non-overlapping destinations within a 64 MB working set.

What comes next#

This is the first post in a series. In the next posts, I will dig into what the measurements have taught me about WebGPU performance and the choices they suggest when writing shaders. For now, try the benchmark on your own devices: those are the costs your code has to work with.