WebGPU vs WebGL2 for UI backgrounds
Whether WebGPU makes an animated gradient faster than WebGL2, with frame times from one engine running both, browser support and a fallback plan.
For an animated gradient background, don't expect WebGPU to be faster than WebGL2. A background is one full-screen fragment shader, and both APIs hand that same shader to the same GPU. When we ran the gradients.sh engine's shaders through both on one test GPU, frame times stayed within about a millisecond of each other. Other GPUs and drivers can differ, so measure on the devices you care about. What WebGPU changes is the plumbing around the shader: one device can feed many canvases, shaders compile without blocking, and it opens a path to compute work. WebGL2 still reaches more browsers (97.1% against 85.8% with full WebGPU support, per caniuse in October 2026), so ship WebGL2 as the base and treat WebGPU as an upgrade on top.
The rest of this post has the measurements, what actually differs between the two APIs for this kind of work, and how to run both without the visitor noticing which one they got.
The measurements
The gradients.sh engine has a WebGL2 backend and a WebGPU backend that run the same maths: each gradient style has a GLSL shader and a WGSL twin. We timed both on an AMD Radeon integrated GPU, in headless Chrome on Linux. Each figure is the time for one frame.
| Style and size | WebGL2 | WebGPU |
|---|---|---|
| Mesh, 1440×900 | 4.7 ms | 5.7 ms |
| Mesh, 2560×1440 | 12.0 ms | 12.1 ms |
| Chrome (metal), 1440×900 | 5.2 ms | 5.6 ms |
| Chrome (metal), 2560×1440 | 14.5 ms | 15.4 ms |
| Pixel glass, 1440×900 | 13.2 ms | 13.4 ms |
| Pixel glass, 2560×1440 | 36.8 ms | 35.6 ms |
Neither column wins. The time grows with the number of pixels and with how much maths the style does per pixel, which is the same on both. The first frame took 7 to 41 ms on either backend.
The output matched too. On that GPU, all nine of the engine's parity test cases came out bit-for-bit identical from WebGL2 in Chrome, WebGPU in Chrome and WebGPU in Node.
We'd expect the same from other background effects that draw one triangle covering the screen and compute each pixel's colour in a fragment shader, though we only measured our own. The GPU spends its time in that shader. The API only decides how the shader and its inputs get there, and for one draw call per frame that overhead is small.
What WebGPU changes for backgrounds
The differences show up around the shader, not in it.
One device drives many canvases. WebGL2 gives each canvas its own context, and browsers cap how many a page can keep. Go past the cap and the browser drops the oldest one, so a page full of gradient cards needs a context pool. The gradients.sh engine keeps at most 8 WebGL2 contexts and copies their frames onto each card's own 2D canvas. With WebGPU, one device renders into any number of surfaces; the engine allows 16 and, in testing, one device drove 24 canvases in 7 ms.
Shaders compile without blocking. device.createRenderPipelineAsync() returns a promise, so a page can keep showing a CSS fallback while the GPU pipeline builds. In WebGL2, the first call that reads a compile or link status, or draws with the program, waits for the compile to finish; the KHR_parallel_shader_compile extension lets you poll instead, where the browser has it.
Loss is per device. A WebGPU device has one device.lost promise that resolves with a reason, and the loss affects every surface that device drew. WebGL2 fires a webglcontextlost event on each canvas separately. Both need handling.
Compute shaders exist. WebGL2 has no compute stage. A background that simulates particles or fluid can run that simulation on the GPU with WebGPU. A gradient that's a function of position and time doesn't need it.
One shader language for the browser and the server. WGSL runs in browsers and in native WebGPU implementations such as Dawn. The gradients.sh preview stills render from the same WGSL in Node through Dawn: 0.2 s per still against 5.7 s through headless Chrome on the same machine.
Where WebGL2 still wins
Reach. caniuse puts WebGL2 at 97.1% of users and full WebGPU support at 85.8%, plus 3.7% partial (October 2026). The gaps are specific:
- Chrome and Edge ship WebGPU on Windows, macOS, ChromeOS and Android. On Linux it depends on the GPU and driver.
- Firefox ships it on Windows, and on macOS 26 or later on Apple Silicon.
- Safari ships it in iOS 26, and on macOS only from macOS 26 Tahoe.
Software adapters. A browser without a usable GPU can still offer WebGPU through a software renderer (SwiftShader, llvmpipe). A full-screen shader on the CPU is slow enough to make the page janky, so you have to detect it, which is the next section.
Less code. A minimal WebGPU draw needs an adapter, a device, a configured context, a pipeline, a uniform buffer and a bind group, and all of it is async. The bare WebGPU half of the gradients.sh engine loads only in browsers that have WebGPU, about 8 KB gzipped, on top of the 52 KB the page already has.
Running both
Pick the backend once per page, before drawing anything, and keep a CSS gradient underneath as the first paint and the last resort.
async function pickBackend() {
if ("gpu" in navigator) {
try {
const adapter = await navigator.gpu.requestAdapter({
powerPreference: "low-power",
});
// Use WebGPU only when the adapter says it is real hardware.
if (adapter?.info?.isFallbackAdapter === false) {
// An adapter isn't a device yet: request one and keep it for drawing.
const device = await adapter.requestDevice();
return { kind: "webgpu", device };
}
} catch {
// No device after all: fall through to WebGL2.
}
}
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");
if (gl) {
gl.getExtension("WEBGL_lose_context")?.loseContext();
return { kind: "webgl2" };
}
return { kind: "css" };
}A few details the snippet only hints at:
- Ask for
low-power. A background doesn't need the discrete GPU, and on a laptop with two GPUs the integrated one saves battery. - Treat an unknown adapter as software.
isFallbackAdapteristruefor software adapters. If it's missing, the browser hasn't told you, so stay on WebGL2. Safari often reports an empty vendor and architecture as well. The gradients.sh engine treats that as unknown too, unless the page forces WebGPU. - Probe before you commit. The engine also draws one 1×1 frame through WebGPU, copies it onto a 2D canvas and reads the pixel back before any gradient uses it. If the colour is wrong, the copy throws or the device takes longer than 5 seconds to arrive, the page stays on WebGL2.
- Plan for device loss. When
device.lostresolves, every surface on that device is gone. The engine keeps each gradient's last frame on screen, tries one new device, and if that fails or the device is lost again, moves the page to WebGL2 for the session. - Load the WebGPU code lazily. Keep the WebGPU half of your renderer behind a dynamic
import()that only runs whennavigator.gpuexists. Browsers without WebGPU then never download it. Browsers that have it but fail the adapter check still do, unless you run the check before the import.
Getting the same pixels from both
If you keep two shaders, one in GLSL and one in WGSL, they drift unless something checks them. Two things caught us:
- Rows run the other way. WebGL's framebuffer counts rows from the bottom, WebGPU's from the top. Functions like
fwidth()work on 2×2 blocks of pixels, so at an odd canvas height the two APIs group different rows together and the result differs along edges. The engine draws one spare row above odd-height frames on WebGPU, which lines the blocks up again. - Dither thresholds. Dithering compares each pixel against a threshold, and a tiny floating-point difference can flip a pixel to the next colour level. Across GPUs and drivers these flips are expected; we count them separately from real colour errors.
The engine's release check renders each test case with WebGL2 in Chrome and with the WGSL through Dawn on the same GPU, and fails unless every pixel matches.
Which one to use
- One gradient behind a hero: WebGL2 is enough. Add WebGPU only if you already have it for something else.
- Many animated gradients on one page, such as a card grid: WebGPU where available, because one device feeds every canvas. Keep a WebGL2 context pool as the fallback.
- A background that simulates something (particles, fluid, flow fields): WebGPU, for compute shaders.
- Plain colour that slides or fades: neither. A CSS gradient costs no JavaScript at all.
- Always: a CSS gradient in the same colours underneath, for the first paint and for browsers with neither API.
In gradients.sh
Every free component already does all of this. It picks WebGPU when the browser passes the checks above and WebGL2 otherwise, draws the same frame on both, and keeps the server-rendered CSS gradient for browsers with neither:
Ship faster
import { MeshGradient } from "@/components/mesh-gradient";
export function MeshGradientDemo() {
return (
<MeshGradient className="flex h-[360px] items-end p-8">
<h2 className="text-3xl font-semibold tracking-tight text-white">
Ship faster
</h2>
</MeshGradient>
);
}
Install it into any React project with a components.json (run npx shadcn@latest init first if you don't have one):
pnpm dlx shadcn@latest add https://gradients.sh/r/mesh-gradient.jsonTo see which backend you get, or to compare them on your own machine, add ?gradient-backend=webgl2 or ?gradient-backend=webgpu to a page's URL. The technical FAQ shows how to set it from code. For a wider look at what other libraries ship and how big they are, see the React gradient libraries comparison.