This is a collection of tests that incrementally add complexity to the setup. Testing is done with Three.js r173 (2025-01-31). All tests use WebGPURenderer, a TSL node, and a test of the graphics backend type used. With vanilla Three.js, React Three Fiber, and Threlte.
Go to a folder, like next15-pages-vanilla-react19
.
If you have Docker installed:
npm run docker
to build and run the app in production mode.
The Docker image uses Node 20, before
navigator
was added in Node 21.
Otherwise, to test with your local Node.js version:
npm i
npm run dev
to check how it works in development.npm run start
to check how it works in production.
A ✅ means the scene renders, and the project works in dev mode, and in production.
next14-app-r3f8-react18
: ✅next14-pages-r3f8-react18
: ✅next15-app-r3f9-react19
: ✅next15-app-r3f9-react19-rsc
: ✅ See this note about RSCsnext15-pages-r3f9-react19
: ✅ Unrelated Next.js HMR warningsveltekit-threlte8
: ✅vite-ts-swc-r3f8-react18
: ✅vite-ts-swc-r3f9-react19
: ✅vite-ts-threlte8
: ✅vite-vanilla-js
: ✅
-
⚠️ Importing a module with top-level await such asthree/examples/jsm/capabilities/WebGPU.js
requires a Vite config change and causes warnings in Next.js. -
⚠️ WebGPURenderer is initialized with WebGPUBackend before falling back to WebGLBackend. You should await the init method before checking the backend type or if encounter this render warning.
Some Three.js modules, like three/examples/jsm/capabilities/WebGPU
, contain top-level await statements.
Importing a module with top-level await will give you this error:
❌
Top-level await is not available in the configured target environment
Add this to your vite.config.js
:
import { defineConfig } from 'vite'
export default defineConfig({
optimizeDeps: { esbuildOptions: { target: 'esnext' } },
build: { target: 'esnext' },
})
One of the options fixes development mode, the other fixes production.
Importing a module with top-level await will give you this warning in the browser console and when compiling:
./node_modules/three/examples/jsm/capabilities/WebGPU.js
The generated code contains 'async/await' because this module is using "topLevelAwait".
However, your target environment does not appear to support 'async/await'.
As a result, the code may not run as expected or may cause runtime errors.
Next.js uses Node.js to Server-Side Render pages on the server. When importing modules on the server, if those modules reference global browser objects like window
, document
, self
, or navigator
at the top level, you will get a compilation error. Except for navigator
, which got added to Node.js 21.
Those top-level references are being tracked down in Three.js for better Next.js support, and this repository is also meant to help testing those issues.
Generally speaking, as a Next.js developer working with libraries that are meant for browsers like Three.js, it is safer to execute browser-only code inside useEffect
hooks or similar. See this article.
import { browserOnlyFunction } from 'three'
browserOnlyFunction() // ❌ Don't do that, it runs on the server during SSR
function MyComponent() {
browserOnlyFunction() // ❌ Don't do that, it runs on the server during SSR
useEffect(() => {
browserOnlyFunction() // ✅ No problem, runs only in the browser
}, [])
return // ...
}
It seems like React Three Fiber 8 is not compatible with Next.js 15 or React 19 in some circumstances.
❌
TypeError: Cannot read properties of undefined (reading 'ReactCurrentOwner')
Also a related error during builds:
❌ Cannot read properties of undefined (reading 'ReactCurrentBatchConfig')
You can use React Server Components with R3F. This actually works without 'use client'
:
<ClientCanvas>
<ClientOrbitControls />
<ClientBox position={[-1.2, 0, 0]} />
<ClientBox position={[1.2, 0, 0]} />
<ambientLight intensity={Math.PI / 2} />
<spotLight
position={[10, 10, 10]}
angle={0.15}
penumbra={1}
decay={0}
intensity={Math.PI}
/>
<pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />
<mesh>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</ClientCanvas>
ClientCanvas
, ClientBox
, and ClientOrbitControls
are marked with 'use client'
. You can interweave server and client components this way, but expect this approach to be pretty painful.
⚠️ [HMR] Invalid message: {"action":"appIsrManifest","data":{}}
TypeError: Cannot read properties of undefined (reading 'pathname')
Fixed in 15.1.1-canary.24
.
WebGPURenderer initially reports WebGPUBackend before falling back to WebGLBackend (issue). There are workarounds for it.
With vanilla Three.js:
renderer = new THREE.WebGPURenderer()
await renderer.init()
console.log(renderer.backend) // WebGPUBackend or WebGLBackend
With React Three Fiber:
<Canvas
gl={async (glProps) => {
const renderer = new WebGPURenderer(glProps)
await renderer.init()
return renderer
}}
/>
You should also expect to only be able to use a subset of Drei and the Three.js ecosystem with WebGPU, since some libraries and composants are written in GLSL.
The following Drei components have been tested with R3F + WebGPU:
-
✅ BakeShadows
-
✅ Billboard
-
✅ Bvh
-
✅ FlyControls
-
✅ GradientTexture
-
✅ Html
-
✅ Instances
-
✅ KeyboardControls
-
✅ MapControls
-
✅ Merged
-
✅ OrbitControls
-
✅ OrthographicCamera
-
✅ Stats
-
✅ StatsGl
-
❌ Edges:
TypeError: Failed to execute 'drawIndexed' on 'GPURenderPassEncoder': Value is infinite and not of type 'unsigned long'.
-
❌ MeshTransmissionMaterial
-
❌ Outlines:
NodeMaterial: Material "ShaderMaterial" is not compatible.
-
❌ Text:
TypeError: Failed to execute 'drawIndexed' on 'GPURenderPassEncoder': Value is infinite and not of type 'unsigned long'.
-
❌ Wireframe: Nothing shows up +
Requires non-indexed geometry, converting to non-indexed geometry.
You can run one of the R3F test cases of this repo and help complete the list. Don't commit code, just edit this README with the results of your tests.
import { mix, vec3, uv } from 'three/tsl'
import { MeshBasicNodeMaterial } from 'three/webgpu'
const red = vec3(1, 0, 0)
const green = vec3(0, 1, 0)
const checkerboard = uv().mul(8).floor().dot(1).mod(2)
const colorNode = mix(red, green, checkerboard)
const material = new MeshBasicNodeMaterial()
material.colorNode = colorNode
import { extend, type ThreeElement } from '@react-three/fiber'
import { mix, positionLocal, sin, time, vec3 } from 'three/tsl'
import { MeshBasicNodeMaterial } from 'three/webgpu'
const red = vec3(1, 0, 0)
const blue = vec3(0, 0, 1)
const currentTime = time.mul(0.5)
const colorNode = mix(red, blue, sin(currentTime))
const positionNode = positionLocal.add(vec3(0, sin(currentTime).mul(0.2), 0))
extend({ MeshBasicNodeMaterial })
declare module '@react-three/fiber' {
interface ThreeElements {
meshBasicNodeMaterial: ThreeElement<typeof MeshBasicNodeMaterial>
}
}
const Plane = () => (
<mesh scale={5}>
<planeGeometry />
<meshBasicNodeMaterial colorNode={colorNode} positionNode={positionNode} />
</mesh>
)