One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

WebGPU

GPU rendering through react-native-webgpu

One ships no GL view. The GPU surface on native is react-native-webgpu, which vendors Dawn on iOS and Android and exposes the web navigator.gpu API plus a Canvas component. Raw WebGPU, TypeGPU, and three.js all build on it; nothing else is needed.

On web the browser’s own WebGPU renders. On native the same API talks to Dawn through the canvas context, presenting each frame by hand.

Usage

import { useEffect } from 'react'
import { Canvas, useCanvasRef } from 'react-native-webgpu'
function GpuSurface() {
const ref = useCanvasRef()
useEffect(() => {
async function draw() {
const canvas = ref.current
if (!canvas) return
const context = canvas.getContext('webgpu')
if (!context) return
const adapter = await navigator.gpu.requestAdapter()
const device = await adapter?.requestDevice()
if (!device) return
context.configure({
device,
format: navigator.gpu.getPreferredCanvasFormat(),
})
// encode passes against device, submit, then:
context.present()
}
void draw()
}, [ref])
return <Canvas ref={ref} style={{ flex: 1 }} />
}

The canvas ref bridges to a context configured like any web canvas. Every frame ends with context.present().

How it works

Three.js runs through three/webgpu with WebGPURenderer, including under React Three Fiber. R3F mounts through createRoot on the webgpu canvas; never use the fiber native entry, which renders through a GL view. On unmount call setAnimationLoop(null) and dispose on the renderer: R3F’s unmount alone never stops three’s internal frame loop.

One three instance must serve the whole graph. On native, bare three resolves to the three/webgpu build and fiber to its web entry, while web keeps WebGL three. The alias option’s native map does this:

import { createRequire } from 'node:module'
import { one } from 'one/vite'
const require = createRequire(import.meta.url)
export default {
plugins: [
one({
alias: {
native: {
three: require.resolve('three/webgpu'),
'@react-three/fiber': require.resolve('@react-three/fiber/dist/react-three-fiber.esm.js'),
},
},
}),
],
}

Metro mode does not apply One’s aliases, so R3F on WebGPU needs the default bundler. WebGPURenderer runs no GLSL: ShaderMaterial and onBeforeCompile do not work under it. Use node materials, or bake static gradients into vertex colors or data textures. PMREMGenerator.fromScene works; equirectangular shader compilation does not exist on this backend.

API reference

PackagesTypeDescription
react-native-webgpuCanvasDawn-backed canvas, context, and gpu API.
CanvasDawn-backed canvas, context, and gpu API.
three/webgpuWebGPURendererThree on WebGPU, no GLSL.
WebGPURendererThree on WebGPU, no GLSL.
@react-three/fiberweb entryR3F mounted through createRoot.
web entryR3F mounted through createRoot.

Platform differences

iOSAndroidWeb
BackendDawnDawnBrowser
SurfaceNative canvasNative canvasHTML canvas
PresentBy handBy handAutomatic
ThreeWebGPU buildWebGPU buildWebGL build

Limits and gotchas

Examples

Render a three scene with fiber

import { useEffect } from 'react'
import { Canvas, useCanvasRef } from 'react-native-webgpu'
import * as THREE from 'three/webgpu'
import { createRoot, events, extend } from '@react-three/fiber'
function Scene() {
return (
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
)
}
function FiberSurface({ size }: { size: { width: number; height: number } }) {
const ref = useCanvasRef()
useEffect(() => {
if (size.width === 0 || size.height === 0) return
let renderer: THREE.WebGPURenderer | null = null
let root: ReturnType<typeof createRoot> | null = null
async function mount() {
const canvas = ref.current
if (!canvas) return
const context = canvas.getContext('webgpu')
if (!context) return
extend({ Mesh: THREE.Mesh, BoxGeometry: THREE.BoxGeometry, MeshStandardMaterial: THREE.MeshStandardMaterial })
renderer = new THREE.WebGPURenderer({ context })
await renderer.init()
const renderFrame = renderer.render.bind(renderer)
renderer.render = ((scene: THREE.Scene, camera: THREE.Camera) => {
renderFrame(scene, camera)
context.present()
}) as typeof renderer.render
root = createRoot(context.canvas)
root.configure({
size: { top: 0, left: 0, ...size },
events,
gl: renderer,
frameloop: 'always',
dpr: 1,
})
root.render(<Scene />)
}
void mount()
return () => {
root?.unmount()
renderer?.setAnimationLoop(null)
void renderer?.dispose()
}
}, [ref, size])
return <Canvas ref={ref} style={{ flex: 1 }} />
}

Wrapping render appends the present call fiber never issues itself. The cleanup unmounts fiber first, then stops three’s loop and disposes the renderer, in that order.

Clear to a color with raw WebGPU

import { useEffect } from 'react'
import { Canvas, useCanvasRef } from 'react-native-webgpu'
function ClearColor() {
const ref = useCanvasRef()
useEffect(() => {
async function clear() {
const canvas = ref.current
if (!canvas) return
const context = canvas.getContext('webgpu')
if (!context) return
const adapter = await navigator.gpu.requestAdapter()
const device = await adapter?.requestDevice()
if (!device) return
const format = navigator.gpu.getPreferredCanvasFormat()
context.configure({ device, format })
const encoder = device.createCommandEncoder()
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
clearValue: { r: 0.1, g: 0.2, b: 0.4, a: 1 },
storeOp: 'store',
},
],
})
pass.end()
device.queue.submit([encoder.finish()])
context.present()
}
void clear()
}, [ref])
return <Canvas ref={ref} style={{ flex: 1 }} />
}

Raw WebGPU needs no three at all: configure the context, encode one clear pass, submit, and present. The same shape scales to full pipelines.

Edit this page on GitHub.