vgpu-shaders — should this page have a shader, and what does that cost?
vgpu is Vercel Labs' WebGPU library "designed for agents": one shader renders in
the browser, headless in Node, to a PNG at any resolution, to video, and in CI. WGSL becomes a module
graph you import like TypeScript — the loader resolves it, reflects bindings, drops unused
declarations and emits compact source at build time.
⛔ First: this skill does not document the vgpu API, on purpose
The API is 258 symbols across 20 packages, and Vercel ships a first-party skill generated from
the source docs, stamped with vgpuVersion and a gitSha in its own frontmatter. Restating it here
would duplicate an artefact that regenerates itself, and would be wrong within a release.
npx skills add vercel-labs/vgpu # the generated API skill
npx -y add-mcp https://vgpu.sh/api/mcp -g # or the MCP
npx -y vgpu docs find <query> | grep -i <term> | cat <symbol>
⚠️ Their skill is named vgpu; this one is vgpu-shaders precisely so both can be installed.
What follows is the half nobody else writes: whether to reach for it, and what you owe the user once you do.
It is not only graphics — but for a dev-flow project it almost always is
WebGPU is a GPU compute API of which rendering is one use, and vgpu exposes both. Compute,
ComputeOptions, StorageBuffer and PingPongStorage are first-class, and initFromDevice(device)
adopts a GPUDevice another library already created — the documented case being ONNX Runtime Web's
WebGPU execution provider, so a model's output stays on the GPU and a shader consumes it with
gpu.device.wrapBuffer(output.gpuBuffer), zero copies. It runs in Node too, with Dawn supplying
WebGPU. Ownership is explicit: vgpu never calls destroy() on a device it did not request.
Know that path exists; then note it is almost never ours. In this skill set the model runs
server-side, through eve and the AI Gateway — client-side inference is a different architecture with
its own bill (model download weight, WebGPU availability, a device the page now owns). It earns
consideration only when the data genuinely must not leave the machine and the client is a browser.
[VERIFY] the ML matrix against the first-party skill (vgpu docs cat ml) before pricing that;
it pins specific webgpu, onnxruntime-web and Dawn versions.
Everything below is the graphics path, because that is the one a dev-flow project actually reaches.
The decision — this is the motion ladder's top rung, not a separate ladder
transitions grades motion in tiers, cheapest first. vgpu is a new Tier 4, above Motion:
| Tier | Tool | Cost |
|---|---|---|
| 0 | tw-animate-css |
a class |
| 1–2 | CSS transitions · View Transitions API | no JS runtime |
| 3 | Motion | a JS runtime, main-thread |
| 4 | vgpu / WebGPU | a GPU device, a compile step, a render loop, and a battery |
Do not skip rungs. A gradient that breathes is Tier 0. A hero that morphs between two states is Tier 2. Reach for Tier 4 only when the artefact is the effect — raymarching, fluid, volumetric lighting, real-time generative texture — and when the page can afford it.
Three questions before you write a line of WGSL:
- Is the effect the product, or decoration? Decoration does not justify a GPU device.
- Who is on the page? A marketing hero seen once tolerates it. A dashboard someone keeps open for eight hours does not — a shader loop is a permanent thermal and battery cost.
- What happens when it doesn't run? WebGPU is browser-only and not universal. There must be a design for the no-WebGPU case, not a blank canvas.
Five uses that are not a hero
The Tier-4 framing above is about decoration. These are the ones where vgpu is the tool because of
what it computes or produces, not because the page needs to look alive. All signatures below are read
off the generated API docs at vgpu@0.3.1.
1 · Compute — no pixels involved. compute(gpu, wgsl, { set, constants, entry }) is a pipeline
with the same reflection and set() ownership rules as a draw, then dispatch(x, y?, z?). Storage is
storage(gpu, bytes, "read" | "read-write") with read(): Promise<ArrayBuffer> and write(data).
Two things the raw WebGPU API makes you hand-roll:
overrideconstants let one JS value drive both@workgroup_sizeand the dispatch maths, so "retuningwgcannot desynchronize them".dispatch({ indirect })takes the workgroup counts from a buffer the GPU wrote, so the CPU never has to learn how many elements survived a compaction pass.
⚠️ The GPU wins on massively parallel arithmetic and loses on transfer. Aggregating 200k dashboard
rows is almost always faster in a Web Worker. The real case is simulation — particles, physics, fields
— where pingPongStorage(gpu, n) + .swap() keeps the data resident between frames and it never makes
the return trip. A complete worked example — WGSL, the loop, the vgpu/mock test, and the
question of whether it was worth it — is in references/compute-example.md.
2 · pixelDiff — visual regression as a utility. Not graphics: QA.
import { pixelDiff } from "@vgpu/render/perf";
const r = await pixelDiff(before, after); // Texture | Uint8Array
if (r.maxByte > 2) throw new Error(`regression (max delta ${r.maxByte})`);
Returns { maxByte, meanByte, changedBytes, totalBytes, changedFraction }. maxByte <= 2 is driver
rounding noise — the threshold that stops a snapshot suite flapping. It accepts plain Uint8Array,
so it compares any two pixel buffers, whatever drew them.
3 · gpuFrameTime — a performance gate in CI.
gpuFrameTime(device, encode, { frames: 120, warmup: 30 }) → { medianMs, meanMs, … }, timestamp
queries where supported and wall clock otherwise. medianMs is "the main number to compare across
builds". Failing a build on frame-time regression is an engineering practice, not an effect.
4 · Server-side image generation from data. The headless path renders to an offscreen target in
Node; target.read() hands back pixels. An OG image per listing, generated from the record, instead
of three hand-made variants that drift. ⚠️ vgpu renders the pixels, not the file — encoding to PNG
is yours (sharp, pngjs).
5 · vgpu/scene — a small scene graph. perspectiveCamera, orbitControls, box/sphere/
torus/icosphere, directionalLight, lambertMaterial, mesh, group. A product configurator or
a navigable room, which is a different job from an animated background.
⚠️ Two honest gaps, both checked 2026-08-26. The compute API's examples in the generated docs all
import from vgpu/mock (the deterministic test entry), not from vgpu — good news for testability,
but [VERIFY] the runtime entry with vgpu docs cat compute before copying an import. And the landing
page advertises MP4 at 60 fps while the shipped docs contain zero occurrences of
video/mp4/encoder/ffmpeg/webm; the headless example is examples/by-example-s13-headless. Treat video
as "read frames, encode them yourself" until proven otherwise.
Animating once you're here: motion/vgpu, not a hand-rolled loop
Motion 13.2.0 (2026-09) added vgpuEffect — a motion/vgpu subpath that binds Motion values
directly to vgpu subjects: shared uniforms ("params.time"), Effect/Draw/Compute bindings, scene
nodes (x/rotateY/scale), cameras, lights, materials, orbit controls and clear colors. Register
it once and animate() targets a shader or scene node like it would a DOM element:
import { animate } from "motion";
import { vgpuEffect } from "motion/vgpu";
animate.addEffect(vgpuEffect); // once, at setup
animate(cube, { rotateY: 360 }, { type: "spring", stiffness: 60 }); // now drives the scene node
This does not change the Tier 3→4 decision above — reaching for vgpu is still justified by the
three questions (product-vs-decoration, who's on the page, the no-WebGPU fallback), never by "Motion
can drive it now." What it changes is the inside of Tier 4: once a shader or vgpu/scene node is
already justified, drive its uniforms/camera/material params with Motion's spring and gesture engine
(useMotionValue + vgpuEffect, drag-to-orbit, scroll-linked uniforms) instead of a hand-rolled
requestAnimationFrame loop recomputing values every frame. Changed values are batched into one
set() per subject per frame in frame.preRender, ahead of vgpu's own frame.render — so it composes
with, rather than fights, vgpu's render loop. module-add motion still installs the runtime; this is
an additional entry point (motion/vgpu) on top of it, relevant only to a project that has already
adopted both. The reduced-motion obligation below is unchanged either way — vgpuEffect gives you a
nicer way to drive values, not an opt-out from freezing them.
⚠️ The obligation the library does not give you: reduced motion
Checked 2026-08-26 across vgpu's getting-started, Next.js and external-ticker guides: they never
mention prefers-reduced-motion. That is not a criticism of the library — it is a gap you inherit.
This repo's golden rule (transitions) is that every transition ships a reduced-motion path, and a
60fps GPU loop is the most aggressive motion we can ship.
The mechanism exists; only the connection is missing. vgpu's external-ticker guide documents
advance(0) as "the honest way to pause: the clock stops, frames still render" — so a reduced-motion
path is render one frame, then stop advancing the clock, not "hide the canvas":
"use client";
// Render the shader's first frame, then hold it. The composition stays; the motion stops.
const reduce = useReducedMotion(); // motion/react, or a matchMedia listener
// … inside the frame loop: advance(reduce ? 0 : deltaSeconds)
A still frame of a good shader is a good image. Hiding the canvas is a worse outcome than never adding it.
Also clamp the delta — the guide's own example does: Math.min(0.25, deltaMs / 1000), because
"a hidden tab must not spiral".
Next.js wiring (the parts that actually bite)
Full guide: references/nextjs-wiring.md. The three that break a build:
.wgslneeds a loader, and Turbopack needsas: "*.js"or it will not treat the loader output as a module. Top-levelturbopack.rulesrequires Next 15.5+; we are on 16, so that holds.init()is browser-only — the canvas lives in a"use client"component andinit()runs in an effect after mount. There is no SSR path; design the server-rendered fallback deliberately.- Bindings live in the entry shader. An imported
.wgslmodule that declares@group/@bindingfails withVGPU-RESOLVE-MODULE-BINDING. Modules export structs and functions; nothing else.
You do not need the loader at all: effect(gpu, source) takes WGSL as a plain string. Reach for it
only when shaders want their own files with imports between them.
Testing — the reason this is worth wiring at all
A shader is the one visual artefact that can be regression-tested exactly, and vgpu is built for it. This is the strongest argument for choosing it over a hand-rolled canvas effect.
- Deterministic frame, not a loop: fixed
dpr: 1,autoResize: false, oneframe(gpu, …), thentarget.read(). Never assert insiderequestAnimationFrame. vgpu/mockfor deterministic unit tests;vgpu/nodeonly when Dawn/WebGPU behaviour is itself under test.- In CI on Linux, WebGPU needs help:
agent-browser --webgpu --headed(SwiftShader, no GPU required). ⚠️ headless Chrome captures the canvas as black — an upstream limitation, so--headedplus Xvfb is not optional. A green headless test that never rendered is the failure mode here.
Hand off the test itself to write-tests; this skill decides that the snapshot exists.
meta.json
"stack": { "shaders": "vgpu" } // only when a shader actually ships
"shaders": {
"surface": "app/(marketing)/page.tsx", // where it renders
"reduced_motion": "clock-freeze", // how the reduced-motion path is served
"fallback": "static-poster", // what a no-WebGPU visitor sees
"snapshot_test": "e2e/hero-shader.spec.ts"
}
No phase bump — a shader is a capability on an existing page, not a stage.
What this skill does NOT do
- Doesn't teach WGSL or the vgpu API — that is the first-party skill and the MCP.
- Doesn't write the shader. Use their
npx vgpu docs/ examples /check/doctorloop. - Doesn't apply to React Native. RN has no WebGPU rung in this skill set; motion there is Reanimated.
- Doesn't override
transitions. If a cheaper tier does the job, that is the answer.
Reference files
references/nextjs-wiring.md— loader config, the client component, the error table.references/compute-example.md— a complete worked compute shader (simulation with resident data), itsvgpu/mockunit test, the two guards that stop it desynchronising — and an honest verdict that the same feature usually belongs in a Web Worker.references/contracts.md— the.workflow/dev-flow contract (vendored).
Sources
- https://vgpu.sh · https://github.com/vercel-labs/vgpu (MIT) — checked 2026-08-26,
vgpu@0.3.1. - First-party skill:
vercel-labs/vgpu→skills/vgpu/SKILL.md, generated, frontmatter carriesvgpuVersion+gitSha.[VERIFY]the API against it, never against this file.