Using vgpu with Next.js and other bundlers
effect(gpu, source) takes WGSL as a string, so nothing forces you to use a bundler loader. Reach for the loader when you want shaders in their own .wgsl files with import between them — the loader resolves that import graph at build time and hands effect() one finished shader.
This guide is the bundler half of Getting started: install, loader config, TypeScript types for .wgsl imports, and the client component that owns the canvas.
Install
npm install vgpuThat is the whole install. @vgpu/wgsl (the loaders) and @vgpu/wgsl-std (the pure-WGSL standard modules) are dependencies of vgpu, so WGSL package imports such as import { voronoi3d } from "@vgpu/wgsl-std/noise"; resolve with no second install step — under npm, pnpm, and Yarn alike, including pnpm's isolated node_modules and Yarn PnP, where transitive packages never appear in your project's node_modules tree.
A WGSL package import resolves from your project's own node_modules first, so a copy you installed yourself always wins, and then next to @vgpu/wgsl itself, which is what makes a transitive install of @vgpu/wgsl-std work. VGPU-WGSL-PKG-NOTFOUND therefore means the package is reachable from neither — install it (npm install <pkg>) and check the specifier spelling. Third-party and workspace packages work the same way, including import { customNoise } from "@packages/shaders"; for a workspace:* package in a monorepo: see Publishing WGSL module packages for the full resolution order and the exports map a package needs.
Next.js with Turbopack
Turbopack is the default dev bundler in current Next.js versions. Register the loader with a top-level turbopack.rules entry; as: "*.js" is required so Turbopack treats the loader output as a JavaScript module:
// next.config.ts
const nextConfig = {
turbopack: {
rules: {
"*.wgsl": {
loaders: ["@vgpu/wgsl/loader-webpack"],
as: "*.js",
},
},
},
};
export default nextConfig;Requires Next.js 15.5 or newer for the top-level turbopack key. Next 15.0–15.2 used experimental.turbo.rules, deprecated since. Turbopack runs webpack-compatible loaders through a bridge: this.addDependency() is not honored there, but @vgpu/wgsl also tracks transitive .wgsl reads through Turbopack's patched fs.readFile, so editing an imported module still invalidates.
The same shape is exercised end to end by examples/next-wgsl in the vgpu repository.
Next.js with webpack
next dev / next build without --turbopack use webpack. Push a rule from the webpack hook — the loader registers itself for test: /\.wgsl$/:
// next.config.ts
type WebpackConfig = { module?: { rules?: unknown[] } };
const nextConfig = {
webpack(config: WebpackConfig) {
config.module ??= {};
config.module.rules ??= [];
config.module.rules.push({
test: /\.wgsl$/,
loader: "@vgpu/wgsl/loader-webpack",
});
return config;
},
};
export default nextConfig;Keep both blocks when your app runs webpack in one command and Turbopack in another: Next reads turbopack only under --turbopack and calls webpack() only without it, so the two configurations coexist. In a project that has Next's own types available, annotate with NextConfig (import type { NextConfig } from "next";) instead of the local WebpackConfig alias; the alias above only exists so this snippet compiles on its own.
Add options: { minify: true } to the rule for production builds. Full option reference: npx vgpu docs cat /@vgpu/wgsl/loader-webpack/index.docs.md.
Vite
// vite.config.ts — wrap in defineConfig() from "vite" if you want its typing
import { wgslVitePlugin } from "@vgpu/wgsl/loader-vite";
export default { plugins: [wgslVitePlugin()] };Type .wgsl imports in TypeScript
TypeScript does not know what a .wgsl module is, so import shader from "./plasma.wgsl" fails with TS2307: Cannot find module until you add an ambient declaration. @vgpu/wgsl ships one — reference it from a .d.ts file anywhere in your project (src/wgsl-env.d.ts is a good spot):
// src/wgsl-env.d.ts
/// <reference types="@vgpu/wgsl/wgsl-types" />Prefer that one-liner: it stays correct if the emitted shape ever changes. If you would rather declare the module yourself — for example to reuse the exported ShaderSource type — put this in src/wgsl-env.d.ts instead. It must be the first statement in the file: a declare module that follows other code is read as a module augmentation and fails with TS2664.
declare module "*.wgsl" {
import type { ShaderSource } from "@vgpu/wgsl";
const source: ShaderSource;
export default source;
}Either way the default export is a ShaderSource object ({ version: 1, wgsl: string }), not a plain string. Pass it straight to effect(gpu, source), which accepts string | ShaderSource — do not reach into .wgsl yourself.
Render it from a client component
WebGPU is browser-only, so the canvas lives in a "use client" component and init() runs in an effect after mount. Keep the vgpu work in a plain function: it is easier to read, and it is the part worth testing.
// src/app/plasma.ts
import { clock, effect, frameLoop, init, surface } from "vgpu";
import type { FrameLoopHandle } from "vgpu";
import plasmaShader from "./plasma.wgsl";
/** Starts the render loop on `canvas`; call the returned function to tear it down. */
export function startPlasma(canvas: HTMLCanvasElement): () => void {
let disposed = false;
let loop: FrameLoopHandle | undefined;
let gpu: Awaited<ReturnType<typeof init>> | undefined;
void (async () => {
gpu = await init();
if (disposed) return gpu.dispose();
const canvasSurface = surface(gpu, canvas, { dpr: [1, 2] });
const plasma = effect(gpu, plasmaShader, {
label: "plasma",
set: { params: { time: 0, texel: canvasSurface.texelSize } },
});
canvasSurface.onResize(() => plasma.set({ params: { texel: canvasSurface.texelSize } }));
const time = clock(gpu);
loop = frameLoop(gpu, (frame) => {
plasma.set({ params: { time: time.time } });
frame.pass(canvasSurface, plasma);
});
})();
return () => {
disposed = true;
loop?.stop();
gpu?.dispose();
};
}// src/app/page.tsx
"use client";
import { useEffect, useRef } from "react";
import { startPlasma } from "./plasma";
export default function Page() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
return startPlasma(canvas);
}, []);
return (
<main style={{ margin: 0, height: "100vh", overflow: "hidden" }}>
<canvas ref={canvasRef} style={{ display: "block", width: "100%", height: "100%" }} />
</main>
);
}The cleanup function matters in development: React's strict mode mounts effects twice, and without stop() plus dispose() you leak a device and a render loop per remount.
Validate before you run the app
Do not use next dev as your shader compiler. Check every .wgsl file — including pure helper modules — with the CLI, which resolves the same import graph the loader does and prints the reflection:
npx vgpu check src/app/plasma.wgslThen prove the pixels in Node instead of squinting at a browser tab: Getting started shows the headless render-and-read-pixels loop, and The default workflow for developing shaders with vgpu is the full playbook.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
TS2307: Cannot find module './x.wgsl' | No ambient declaration for .wgsl | Add the wgsl-env.d.ts above |
VGPU-WGSL-PKG-NOTFOUND: Package <pkg> was not found | A WGSL package import is not installed in node_modules | npm install <pkg>, or fix the specifier |
VGPU-WGSL-RUNTIME-IMPORT | The bundler ran the loader synchronously while the WGSL file has top-level imports | Let the loader use async mode (webpack/Turbopack/Vite all do by default) |
VGPU-RESOLVE-MODULE-BINDING | An imported .wgsl module declares @group/@binding | Keep resources in the entry shader; modules export only structs and functions |
| Shader compiles but nothing draws | Passing source.wgsl (a string field) where the object was expected, or no frame.pass | Pass the imported object to effect(gpu, source); render inside frame/frameLoop |
See also
npx vgpu docs cat /@vgpu/wgsl/loader-webpack/index.docs.md— every loader optionnpx vgpu docs cat /@vgpu/wgsl/loader-vite/index.docs.md— the Vite pluginnpx vgpu docs cat /@vgpu/wgsl/runtime/resolve-shader.docs.md— resolving import graphs without a bundlernpx vgpu docs cat /@vgpu/wgsl-std/noise/index.docs.md— WGSL modules you can import by package name- Publishing WGSL module packages — ship your own
.wgslmodules as a package, or share them across a monorepo