2025-09-27Bruno Fernandes

WebGPU in Next.js Part 1: Setup

Safari finally oficially supports WebGPU as of version 26. Although it's still not widely available across all browsers (it's still gated behind a flag in Firefox), I thought it would be a good opportunity to experiment with it in a Next.js project. I have some experience with compute shaders in Unity, so I think it'll be fun see how that translates over to wgsl.

The project is available on GitHub. It has a simple playground for playing with a render pipeline (fragment + vertex shaders) and a compute shader. I plan on expanding it to include sampling a texture created in a compute shader in the render pipeline.

You can see a demo here (please ignore the ugly styling!): https://nextjs-webgpu-playground.netlify.app/

In this series of posts, I'll go over how I implemented them.

1. Boilerplate

To get started, I used my own Next.js template. It has all of my preferred Next.js boilerplate and configuration, including prettier, eslint, tailwind, lint-staged and more, all up to date.

1npx create-next-app@latest --example https://github.com/brunocpf/nextjs-template nextjs-webgpu-playground

I also went ahead and installed the WebGPU TypeScript type definitions, which are still not available by default.

1npm install --save-dev @webgpu/types

To make these types globally available in the project, I added them to the types field in my tsconfig.json file:

tsconfig.json

1{
2  "compilerOptions": {
3    ...
4    "types": ["@webgpu/types"],
5    ...
6  }
7}

2. GPU Provider

Everything in WebGPU starts by getting a a GPUDevice, which is the logical connection to our GPU in our code. Let's create a function that can initialize that connection.

src/lib/webgpu.ts

1export type WebGpuContext = {
2  adapter: GPUAdapter;
3  device: GPUDevice;
4  supportedFeatures: GPUSupportedFeatures;
5};
6
7export async function initWebGPU(
8  requestedFeatures: GPUFeatureName[] = [],
9): Promise<WebGpuContext> {
10  if (!("gpu" in navigator)) {
11    throw new Error("WebGPU not supported in this browser.");
12  }
13
14  const adapter = await navigator.gpu.requestAdapter();
15  if (!adapter) throw new Error("No GPU adapter found.");
16
17  const supported = adapter.features;
18  const features = requestedFeatures.filter((f) => supported.has(f));
19
20  const device = await adapter.requestDevice({ requiredFeatures: features });
21  return {
22    adapter,
23    device,
24    supportedFeatures: adapter.features,
25  };
26}

In this function, first we check if the browser supports WebGPU. Then we request features (like shader-f16) only if the adapter (virtual/physical GPU) supports them. Asking for unsupported features fails device creation. Then we return an object with a handle to the connection to the device.

Let's create a React context so that we can use this handle in our React components.

src/providers/gpu-provider.ts

1"use client";
2
3import { createContext, use, useEffect, useState } from "react";
4
5import { initWebGPU } from "@/lib/webgpu";
6
7export const GpuContext = createContext<{
8  device?: GPUDevice;
9  error?: string;
10}>({});
11
12export function GpuProvider({ children }: React.PropsWithChildren) {
13  const [device, setDevice] = useState<GPUDevice>();
14  const [error, setError] = useState<string>();
15
16  useEffect(() => {
17    let cancelled = false;
18    (async () => {
19      try {
20        const ctx = await initWebGPU(["shader-f16"]);
21        if (!cancelled) {
22          setDevice(ctx.device);
23        }
24      } catch (e: unknown) {
25        if (!cancelled) setError(e instanceof Error ? e.message : String(e));
26      }
27    })();
28    return () => {
29      cancelled = true;
30    };
31  }, []);
32
33  return (
34    <GpuContext.Provider value={{ device, error }}>
35      {children}
36    </GpuContext.Provider>
37  );
38}
39
40export const useGpu = () => use(GpuContext);
41

I'll go ahead and add it to the root layout component:

src/app/layout.tsx

1import { GpuProvider } from "@/providers/gpu-provider";
2
3import "./globals.css";
4
5export const metadata = { title: "WebGPU Compute Playground" };
6
7export default function RootLayout({
8  children,
9}: {
10  children: React.ReactNode;
11}) {
12  return (
13    <html lang="en" suppressHydrationWarning>
14      <body className="font-sans">
15        <GpuProvider>
16          <main className="m-3 rounded border border-gray-300 p-3">
17            <h1 className="text-4xl font-bold">
18              Welcome to the WebGPU Playground
19            </h1>
20            <p>Explore the power of GPU computing in your browser.</p>
21            {children}
22          </main>
23        </GpuProvider>
24      </body>
25    </html>
26  );
27}
28

Doing this will create context that keeps a single reference to an instance of the GPUDevice (initialized when the provider mounts) in state, so that we can access it in any client component in our app.

3. Next Steps

Now that we have a reference to the GPU, we can start writing our shaders and implement a render pipeline. I'll go over that in the next posts. See you there.