This is a collection of tests that incrementally add complexity to the setup. Testing is done with Three.js r171 (2024-11-29). All tests use WebGPURenderer, a call a TSL function, 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
(you might need--legacy-peer-deps
React 19)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 warningnext15-pages-vanilla-react19
: ✅sveltekit-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 your wrapper such as R3F tries to render before the backend is initialized. With R3F, you can useframeloop="never"
to delay the first render call. If you don't, you will get this render warning. -
⚠️ Using React Three Fiber with React 19 requires installing withnpm i --legacy-peer-deps
. -
⚠️ Using R3F v9 requires a fix when initializing the canvas.
The following test cases are less relevant now:
next15-app-r3f8-react18
: ❌ReactCurrentOwner
errornext15-app-vanilla-react19
: ✅next15-pages-r3f8-react18
: ✅ Unrelated Next.js HMR warningnext15-pages-r3f8-react19
: ❌ReactCurrentOwner
error
React Three Fiber normally creates a WebGLRenderer with these defaults (see the code). Same thing with Threlte. For a similar setup with WebGPURenderer, pass the same parameters to the WebGPURenderer constructor:
const renderer = new WebGPURenderer({
canvas,
powerPreference: 'high-performance',
antialias: true,
alpha: true,
})
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.
This warning is caused by using R3F with WebGPURenderer.
⚠️ THREE.Renderer: .render() called before the backend is initialized. Try using .renderAsync() instead.
There is a workaround:
const [frameloop, setFrameloop] = useState('never')
<Canvas
frameloop={frameloop}
gl={(canvas) => {
const renderer = new WebGPURenderer({ canvas })
renderer.init().then(() => setFrameloop('always'))
return renderer
}}
/>
If you use R3F v9, you will get this error on your Canvas:
❌
TypeError: gl.xr.addEventListener is not a function
It can be fixed with:
<Canvas
gl={canvas => {
const renderer = new WebGPURenderer({ canvas })
renderer.xr = { addEventListener: () => {} }
return renderer
}}
>
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')
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:
const [frameloop, setFrameloop] = useState('never')
<Canvas
frameloop={frameloop}
gl={(canvas) => {
const renderer = new WebGPURenderer({ canvas })
renderer.init().then(() => setFrameloop('always'))
return renderer
}}
/>
If checking the backend type is not critical (for example you just want to see which one is used when developing locally) you can use a setTimeout
to keep things simple:
setTimeout(() => {
console.log(
renderer.backend.isWebGPUBackend ? 'WebGPU Backend' : 'WebGL Backend'
)
}, 1000)
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>
)