v10 deployment.Multi-Canvas
v10 supports sharing a single WebGPURenderer across multiple Canvas components. This enables HUD overlays, picture-in-picture views, and multi-viewport rendering without the overhead of multiple GPU contexts.
import { Canvas, useFrame, useThree } from '@react-three/fiber'
Renderer sharing is WebGPU-only, which every Canvas from @react-three/fiber is (and from the deprecated @react-three/fiber/webgpu). @react-three/fiber/legacy has no primary or share.
Basic Setup
Mark the canvas that owns the renderer primary. Every other canvas shares its renderer automatically:
<Canvas primary renderer={{ shadows: 'soft', toneMapping: THREE.AgXToneMapping }}>
<Scene />
</Canvas>
<Canvas>
<SecondaryScene />
</Canvas>
The primary creates the WebGPURenderer; each other canvas borrows it and draws into its own <canvas> element through a three.js CanvasTarget. Renderer-wide settings (everything in renderer: shadows, tone mapping, output color space, constructor options such as antialias) belong on the primary.
With no canvas marked primary, nothing changes: every canvas creates its own renderer, as without multi-canvas.
Who Owns, Who Shares
When a canvas creates its renderer, it shares a primary's if:
- it names one with
share="id", or - exactly one
<Canvas primary>is mounted (or mounting in the same commit), and the canvas did not opt out.
A canvas owns its renderer instead when:
- it is the
primary - it passes
share={false} - it passes its own renderer instance or factory (
renderer={myRenderer},renderer={(defaults) => ...}) - it uses the
glprop (WebGL can't share) - no primary is mounted
A renderer props bag does not opt out: a canvas sharing the primary's renderer ignores its bag (and warns once in development, "renderer settings on a sharing canvas are ignored; set them on the <Canvas primary>"), because settings on a shared renderer would change every canvas drawing with it. Pass share={false} if that canvas should have its own renderer with those settings. textureColorSpace is the one exception: it is per-canvas texture-loading state, not a renderer setting, so a sharing canvas keeps its own.
primary and share are read when the canvas creates its renderer. Changing them on a mounted canvas has no effect until it remounts.
Several primaries
Give each primary an id, and point the other canvases at one with share:
<Canvas id="left" primary>...</Canvas>
<Canvas id="right" primary>...</Canvas>
<Canvas share="left">...</Canvas>
<Canvas share="right">...</Canvas>
Automatic sharing only works with a single primary: with several mounted, a canvas without share throws, asking you to pick one. Two canvases marked primary without distinct ids throw too; the first keeps the renderer.
share="id" waits for that primary to mount, so it can appear anywhere in the tree or in a later commit. It times out after 5 seconds with an error naming the missing <Canvas id="..." primary>.
Timing
Sharing is decided once, when a canvas creates its renderer, so it matters what is mounted by then:
- Same commit, any order. A primary announces itself as soon as it mounts, before any canvas in that commit creates a renderer. A canvas placed before its primary in the tree still waits for it and shares.
- Primary first. A canvas mounting later shares it straight away.
- Primary later. Canvases that already created their own renderer keep it: they are not adopted after the fact. The primary logs one development warning when this happens. Mount the primary with or before the canvases that share it, or give them
share="id"so they wait for it.
StrictMode, Fast Refresh (of your components or of R3F itself) and HMR keep the same renderer and the same sharing; they don't create a second renderer or leave canvases waiting.
When the primary unmounts first, its sharing canvases keep rendering: each holds a lease on the renderer, which is disposed with the last canvas using it. A remounted primary creates a new renderer; canvases that were sharing the old one keep it until they remount, and canvases mounting afterwards share the new one.
Scheduling
A sharing canvas renders after its primary by default. Order it yourself, or cap its frame rate, with the scheduler Canvas prop:
// Limit to 30fps (useful for HUDs that don't need full framerate); still after the primary
<Canvas scheduler={{ fps: 30 }}>
// Render before the primary instead
<Canvas scheduler={{ before: 'main' }}>
before / after take Canvas ids and replace the default ordering. scheduler is a Canvas prop on every entry, not only for multi-canvas; see Frame Loop.
Accessing the Primary Scene (HUD Pattern)
A secondary canvas can render the primary's scene from a different viewpoint using primaryStore from R3F state. This is the basis for HUDs and picture-in-picture.
import { Canvas, useFrame } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import * as THREE from 'three/webgpu'
function HudScene() {
const orthoCamera = useRef<THREE.OrthographicCamera>(null!)
useEffect(() => {
const cam = new THREE.OrthographicCamera(-3, 3, 3, -3, 0.1, 100)
cam.position.set(0, 10, 0) // Above the scene
cam.lookAt(0, 0, 0)
cam.updateProjectionMatrix()
orthoCamera.current = cam
}, [])
// Take over rendering to draw the primary's scene with our own camera
useFrame(
({ primaryStore, renderer }) => {
if (!orthoCamera.current) return
const primaryState = primaryStore.getState()
renderer.render(primaryState.scene, orthoCamera.current)
},
{ phase: 'render' },
)
return null
}
function App() {
return (
<>
{/* Main 3D scene: owns the renderer */}
<Canvas id="main" primary>
<ambientLight />
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</Canvas>
{/* HUD overlay - top-down view of the same scene, sharing the renderer */}
<div style={{ position: 'absolute', top: 10, left: 10, width: 200, height: 200 }}>
<Canvas scheduler={{ fps: 30 }}>
<HudScene />
</Canvas>
</div>
</>
)
}
How It Works
Each canvas maintains its own scene graph and camera, event handling, and state (its own Zustand store).
Secondary canvases share the WebGPURenderer instance and its GPU resources/context. Renderer-wide settings come from the primary, which owns the renderer: a secondary sharing it does not apply its own, so it cannot reset the primary's.
The primaryStore on each canvas's state provides access to the primary's scene/camera, enabling advanced patterns like HUDs. TSL resources (uniforms, nodes, buffers, GPU storage) are shared automatically: they live on the primary's RootState, each secondary holds the same map objects, and portals inherit them from their parent, so state.uniforms (and the @react-three/tsl hooks) read the same set on every canvas and portal.
R3F automatically manages CanvasTarget switching across canvases. A job in the scheduler's start phase sets the correct canvas target before any other jobs run for that root, so user render callbacks render to the right canvas automatically.
WebGL2 Fallback
When navigator.gpu is unavailable, three's WebGPURenderer silently falls back to its WebGL2 backend. A WebGL context is bound to the canvas element it was created on, so a renderer cannot be shared across canvases in that environment. A secondary whose primary fell back therefore creates its own renderer, forced onto the same WebGL2 backend (forceWebGL), and renders independently: scenes stay correct, at the cost of one GPU context per canvas instead of one shared.
Such a fallback secondary owns its renderer, so it is a standalone root: it is not flagged internal.isSecondary (which marks a canvas borrowing another's renderer), it applies its own renderer settings (shadows included), and unmounting it disposes the renderer R3F built for it, whether or not the primary is still mounted. It does not inherit the primary's settings: set them on it too if they should match. internal.sharedRendererFallback records the fallback. GPU resources cannot cross GL contexts, so TSL resources stay local (primaryStore self-references, so the HUD pattern above sees the secondary's own scene), and a fallback secondary is never registered as a shareable primary for further canvases.
API Reference
Canvas props:
State properties:
Related
- WebGPU Overview - the
rendererprop and entry points - Frame Loop - the
schedulerprop and root ordering - Render Pipeline - post-processing per canvas