You're viewing the v10 deployment.

v10 Migration Guide

Upgrading to v10 - WebGPU support, new scheduler, and what changes for you

Alpha Release - v10 is currently in alpha. We'd love your feedback!

The Short Version

@react-three/fiber now renders with WebGPU. Every <Canvas> from the default import uses three's WebGPURenderer, which falls back to a WebGL2 backend where the browser has no WebGPU, so it runs everywhere v9 did. What it cannot run is GLSL: ShaderMaterial, RawShaderMaterial, onBeforeCompile, and libraries built on them (much of drei's shader-based components, postprocessing). Those need WebGLRenderer, which is now only on @react-three/fiber/legacy.

So the first decision is one import:

  • Your scene uses GLSL or WebGL-only libraries: change @react-three/fiber to @react-three/fiber/legacy wherever you import Canvas (and createRoot). Everything else in this guide still applies, and the rest of your app and libraries keep importing hooks from @react-three/fiber.
  • It doesn't: keep the import. Standard materials, lights, shadows, loaders and controls work on WebGPURenderer unchanged.

A few other things break, and most of them fail loudly rather than silently: <Canvas shadows> throws (the setting moved into gl / renderer), gl={...} on a @react-three/fiber Canvas throws (it configures WebGLRenderer), the onUpdate prop is gone, and the dependency ranges moved. Three changes are quieter: a GLSL material on a WebGPU canvas renders blank (with a development warning), a useFrame job in the physics phase now runs on a fixed timestep, and negative numeric useFrame priorities run in the opposite order from v9 (with a warning). The checklist below covers all of them.

Quick Checklist

@react-three/eslint-plugin's migration config finds most of these in your code, and --fix rewrites many of them: add configs.migration to your eslint.config.js while you upgrade (see the plugin readme).

Upgrading from an earlier v10 alpha? Also:

  • Import the TSL hooks from @react-three/tsl (see TSL hooks moved)
  • Replace renderer={{ primaryCanvas }} and renderer={{ scheduler }} with <Canvas primary> and <Canvas scheduler> (see Multi-Canvas Props)
  • Pass { persist: true } to useRenderPipeline where post-processing should outlive the component that set it up (see TSL hooks moved)
  • Drop the renderer boolean: <Canvas renderer> is now the same as <Canvas>. An app on @react-three/fiber without it, which was on WebGL, is now on WebGPU
  • Move imports from the deprecated @react-three/fiber/webgpu (and @react-three/test-renderer/webgpu) to @react-three/fiber (and @react-three/test-renderer); its hooks are already typed for WebGPU (see Hooks Typed for WebGPU). They are removed in the first beta

WebGPU needs no manual init. Pass renderer parameters or a pre-created renderer instance through renderer, and R3F handles initialization:

// Pass parameters
<Canvas renderer={{ antialias: true, forceWebGL: false }}>

// Or a renderer instance (still no init needed)
<Canvas renderer={myWebGPURenderer}>

Breaking Changes at a Glance

Every change below is covered in its own section. The same list, with the alpha each one landed in, is on its own page: v10 Breaking Changes. Silent changes alter behaviour or types without an error, so check for those.

ChangeWhat to do
@react-three/fiber renders with WebGPUKeep it, or import Canvas from /legacy for GLSL and WebGL-only libraries
Silent: GLSL materials render blank on a WebGPU canvas/legacy, or port to a node material (warns once in development)
gl={...} on a @react-three/fiber Canvas throws/legacy, or move settings to renderer={{ ... }}
Hooks are typed for WebGPUNothing for most apps; libraries for both renderers type against /extension
/webgpu entry deprecatedImport from @react-three/fiber
Dependency ranges: three >=0.185.0, React >=19.0 <19.4Upgrade three
React Native is @react-three/nativeChange the import
state.gl deprecatedstate.renderer
state.clock removedstate.time / delta / elapsed
Silent: numeric useFrame priority runs higher firstPhases or before / after
Silent: physics phase is a fixed timestepStep by the delta you get
legacy, linear, flat, colorSpace, toneMapping props removedRenderer settings in renderer / gl
shadows prop throwsrenderer={{ shadows }} / gl={{ shadows }}
onUpdate removedAn effect, useFrame or a ref callback
Split gain map environmentsSingle-file Ultra HDR .jpg
Multi-canvas props (alpha upgraders)<Canvas primary>, share, <Canvas scheduler>
TSL hooks (alpha upgraders)Import from @react-three/tsl

Breaking Changes

@react-three/fiber Renders With WebGPU

The default import renders every <Canvas> with WebGPURenderer. In v9, and in the v10 alphas before alpha.6, it rendered with WebGLRenderer unless you passed renderer. WebGLRenderer now lives only on @react-three/fiber/legacy, and the import is the switch: there is no prop that turns a default-import Canvas back into WebGL.

// GLSL shaders, onBeforeCompile, postprocessing, drei's shader-based components
- import { Canvas } from '@react-three/fiber'
+ import { Canvas } from '@react-three/fiber/legacy'

Change it where you import Canvas or createRoot. Hooks (useThree, useFrame, useLoader, ...) can keep coming from @react-three/fiber: every entry shares one core, so they work under a /legacy Canvas, and libraries that import from @react-three/fiber do too.

What tells you which one you need:

  • GLSL materials. WebGPURenderer compiles node materials (TSL), not GLSL. A ShaderMaterial or RawShaderMaterial on a WebGPU canvas is drawn with a blank default material, three logs NodeBuilder: Material "ShaderMaterial" is not compatible, and R3F warns once per material type in development, naming /legacy. onBeforeCompile is ignored. Standard materials (MeshStandardMaterial, MeshPhysicalMaterial, ...) are converted to their node equivalents automatically.
  • The gl prop. It configures WebGLRenderer, so on a @react-three/fiber Canvas it throws, naming /legacy. Move renderer settings to renderer={{ ... }} to stay on WebGPU.
  • Libraries. Anything that needs a WebGLRenderer (postprocessing's EffectComposer, GLSL-based drei components, code calling WebGL-only renderer APIs) needs /legacy until it supports WebGPURenderer. state.gl on a WebGPU canvas aliases the WebGPURenderer, with a one-time deprecation notice: code that only reads gl.domElement or gl.setSize keeps working, WebGL-only calls do not.

Node materials go the other way: on /legacy they throw an error pointing back to @react-three/fiber.

Hooks Typed for WebGPU

useThree, useFrame, useRenderTarget and Canvas's onCreated from @react-three/fiber are typed for the renderer that import renders with: state.renderer is a WebGPURenderer, and WebGPU-only members need no cast or declaration. The JSX elements are three's three/webgpu namespace, so node materials are typed and <webGLRenderer> is not.

import { useFrame, useThree } from '@react-three/fiber'

const renderer = useThree((s) => s.renderer) // WebGPURenderer
useFrame(({ renderer }) => renderer.compute(kernel))

RootState itself is still the shared base type (either renderer), so packages that augment it keep working, and code annotated with it still compiles: useFrame((state: RootState) => ...) is fine. The deprecated state.gl keeps its base type; use state.renderer.

What this breaks is code that relied on the union from the default import, such as reading state.renderer as a WebGLRenderer. That code would fail at runtime on a WebGPU canvas anyway; it belongs on /legacy, whose hooks are typed for WebGLRenderer. Libraries that support both renderers type against @react-three/fiber/extension, which keeps the union. See Typing the renderer.

Dependency ranges moved

The first thing you will hit, before any code change: v10 will not install alongside the versions v9 accepted.

Peerv9v10
three>=0.156>=0.185.0
react>=19.0>=19.0 <19.4

The three floor moved because v10 uses three's own RenderPipeline (renamed from PostProcessing in r183), CubeRenderTarget, and the real UniformNode types, rather than shadowing them. If you are on r181–r184 you cannot install v10 — upgrade three alongside R3F.

The React ceiling states the versions we have tested (19.0 through 19.3), and moves when a newer one is verified.

React Native moved to its own package

@react-three/fiber/native is now @react-three/native, versioned independently.

Warning

@react-three/native is not on npm yet; its source lives at pmndrs/native. v10 has no /native entry, so React Native apps stay on v9 until it is published.

- import { Canvas, useFrame } from '@react-three/fiber/native'
+ import { Canvas, useFrame } from '@react-three/native'

gl renamed to renderer

The gl property in state has been renamed to renderer to be renderer-agnostic and match Three.js conventions.

function MyComponent() {
  const { renderer } = useThree()
-  const { gl } = useThree()

  // Or in useFrame
-  useFrame(({ gl }) => {
+  useFrame(({ renderer }) => {
    // ...
  })
}

Note: state.gl still works. On a WebGPU canvas it aliases state.renderer (the WebGPURenderer) and logs a one-time deprecation notice; on /legacy it is the WebGLRenderer, as before, with no notice. Prefer state.renderer in new code.

Renderer Selection

Your Use CaseImport FromRenderer
Default@react-three/fiberWebGPURenderer, downloaded on demand
GLSL shaders, WebGL-only libraries@react-three/fiber/legacyWebGLRenderer, no extra request
Deprecated, removed in the first beta@react-three/fiber/webgpuWebGPURenderer, imported statically
// Default - WebGPURenderer, fetched when the first Canvas mounts
import { Canvas, useFrame } from '@react-three/fiber'

// Legacy - WebGLRenderer only, typed as such
import { Canvas, useFrame } from '@react-three/fiber/legacy'

// TSL hooks come from @react-three/tsl on either WebGPU import
import { useUniforms } from '@react-three/tsl'

All three entries share one core, so libraries built on @react-three/fiber work under a Canvas from any of them without adding a renderer to your bundle.

useFrame Clock Removed

The state.clock (THREE.Clock) has been removed from FrameState. Timing information is now provided directly from the RAF timer, which is more accurate and consistent.

useFrame((state, delta) => {
-  const elapsed = state.clock.getElapsedTime()
-  const delta = state.clock.getDelta()
+  const { time, delta, elapsed } = state
+  // time: High-resolution timestamp from RAF (milliseconds)
+  // delta: Time since last frame (seconds)
+  // elapsed: Elapsed time since first frame (seconds)
})

Render Phase Takes Over Rendering

In v9, passing a numeric priority > 0 to useFrame disables R3F's default rendering, requiring you to render manually. In v10, this behavior is preserved for backwards compatibility, but emits a deprecation warning.

The recommended v10 mechanism is registering a callback with { phase: 'render' }. This also disables R3F's default rendering, so your callback is responsible for rendering the scene. Existing numeric-priority callbacks that render manually still work; migrate them to the render phase to avoid the warning.

// v9: Numeric priority > 0 takes over rendering (still supported in v10, but deprecated)
useFrame(({ gl, scene, camera }) => {
  gl.render(scene, camera)
}, 1)

// v10: Use the render phase explicitly
useFrame(
  ({ renderer, scene, camera }) => {
    renderer.render(scene, camera)
  },
  { phase: 'render' },
) // Render phase takes over

For ordering without taking over rendering, use phases or before/after constraints. In the legacy numeric form, only priorities of 0 or less leave the default render enabled; a positive priority takes over rendering even if the callback does not render anything.

// v9: Order callbacks without disabling the default render
useFrame(() => physicsStep(), -2)
useFrame(() => updateGame(), -1)
useFrame(() => updateAI(), 0)

// v10: Phase-based ordering (recommended)
useFrame(() => physicsStep(), { phase: 'physics' })
useFrame(() => updateGame(), { phase: 'update' })
useFrame(() => updateAI(), { after: 'update', before: 'render' })

In the v10 form, physicsStep runs on a fixed timestep: see the next section.

Ordering direction is reversed. A bare number is mapped to { priority: n }, and v10 runs higher priority first (v9 ran lower first). Left unmigrated, the v9 example above runs updateAI first and physicsStep last on v10. Only priority > 0 (render takeover) and negative values (reversed ordering) emit a deprecation notice; a bare 0 is the default and is silent. Migrate to phases or before/after rather than to { priority }: a number is only a loose tie-break and doesn't guarantee order against other jobs.

physics Runs on a Fixed Timestep

The built-in physics phase steps at a fixed 1 / 60 of a second (from @pmndrs/scheduler 0.3). A job in it runs once for every whole step of time that has built up, not once per frame: on a 120 Hz display it skips every other frame, on a slow frame it runs several times (at most 8, after which the surplus time is dropped), and it always gets delta === 1 / 60. This is what a physics engine wants, and it makes the simulation run at the same speed on every display.

- useFrame(() => world.step(1 / 60), { phase: 'physics', fps: 60, drop: false })
+ useFrame((_, delta) => world.step(delta), { phase: 'physics' })

Work that has to run exactly once per frame belongs in another phase (update is the default). To interpolate what you render between the last two physics states, read state.overstep, and to make physics run once per frame again, call getScheduler().setPhaseTimestep('physics', undefined). See Frame Loop.


Removed Props

Renderer Props Removed

The following Canvas props have been removed. Use the gl (WebGL LEGACY ONLY) or renderer (WebGPU) props to pass these settings directly to the renderer:

Removed props:

  • legacy - THREE.ColorManagement.enabled is now always true (modern Three.js default)
  • linear - Use renderer={{ outputColorSpace: THREE.LinearSRGBColorSpace }}
  • flat - Use renderer={{ toneMapping: THREE.NoToneMapping }}
  • colorSpace - Use renderer={{ outputColorSpace: ... }}
  • toneMapping - Use renderer={{ toneMapping: ... }}
- <Canvas linear />
+ <Canvas renderer={{ outputColorSpace: THREE.LinearSRGBColorSpace }} />

- <Canvas flat />
+ <Canvas renderer={{ toneMapping: THREE.NoToneMapping }} />

- <Canvas colorSpace={THREE.LinearSRGBColorSpace} toneMapping={THREE.NoToneMapping} />
+ <Canvas renderer={{ outputColorSpace: THREE.LinearSRGBColorSpace, toneMapping: THREE.NoToneMapping }} />

Why the change? These props were redundant with what you can already pass via gl/renderer. Removing them simplifies the API and makes it clear that renderer settings should be configured through the renderer props.

Accessing values: If you were reading these from useThree(), access them from the renderer directly:

- const { colorSpace, toneMapping, legacy, linear, flat } = useThree()
+ const { renderer } = useThree()
+ const colorSpace = renderer.outputColorSpace
+ const toneMapping = renderer.toneMapping

Note: textureColorSpace survives, but not as a Canvas prop. It lives in ColorManagementConfig, intersected into GLProps and RendererProps, so it goes inside the config bag rather than at the top level:

- <Canvas textureColorSpace="srgb" />
+ <Canvas gl={{ textureColorSpace: 'srgb' }} />
+ <Canvas renderer={{ textureColorSpace: 'srgb' }} />

shadows Moved into the Renderer Settings

Shadows are a renderer-wide setting, so shadows moved from the Canvas into the gl / renderer props bag, next to toneMapping and the other renderer settings. It accepts the same values. The old prop throws an error naming the new place.

- <Canvas shadows="variance" />
+ <Canvas gl={{ shadows: 'variance' }} />       // WebGL, @react-three/fiber/legacy
+ <Canvas renderer={{ shadows: 'variance' }} /> // WebGPU

R3F applies it to a renderer it builds. A renderer instance or factory you pass keeps the shadowMap you configured on it.

Multi-Canvas Props (alpha upgraders)

Earlier v10 alphas configured renderer sharing inside the renderer bag. It is now a pair of Canvas props, and scheduling is its own Canvas prop on every entry. There is no deprecation path: the old keys throw, naming the new API.

- <Canvas id="main" renderer>
- <Canvas renderer={{ primaryCanvas: 'main', scheduler: { fps: 30 } }}>
+ <Canvas id="main" primary>
+ <Canvas scheduler={{ fps: 30 }}>     // shares the primary automatically, renders after it
  • renderer={{ primaryCanvas }} → mark the owner <Canvas primary>; other WebGPU canvases share it automatically. With several primaries, pick one with share="id"; opt out with share={false}.
  • renderer={{ scheduler }} → <Canvas scheduler> (also createRoot().configure({ scheduler }), and now available on /legacy).
  • renderer means only true, a props bag, an instance or a factory, and a props bag configures a renderer the canvas owns: a canvas sharing the primary's renderer ignores it (with a development warning). Renderer-wide settings belong on the primary.
  • Mind the direction when ordering canvases with numbers: scheduler={{ order }} runs lower values first, while a useFrame job's priority runs higher values first. Prefer before / after with canvas ids for both.

See Multi-Canvas.

onUpdate Removed

The onUpdate prop is gone. It was called after every prop update as a hook into the reconciler's internals, but it fired for reasons that had nothing to do with the props you cared about (attach changes, Suspense hide/unhide, child mounts) and not for the ones that did (imperative mutation, anything set in a useFrame). What you want is one of these three, all plain React or R3F:

An effect keyed on the props that change. The effect runs exactly when those values change, and you can read the object through its ref.

- <bufferGeometry onUpdate={(self) => self.computeVertexNormals()} />
+ const ref = useRef<THREE.BufferGeometry>(null)
+ useEffect(() => ref.current?.computeVertexNormals(), [positions])
+ <bufferGeometry ref={ref}>
+   <bufferAttribute attach="attributes-position" args={[positions, 3]} />
+ </bufferGeometry>

Read the object in a useFrame loop. For continuous work, or anything that has to react to mutations made outside React, read the object each frame instead of waiting for a render.

const ref = useRef<THREE.Mesh>(null)
useFrame(() => {
  ref.current!.rotation.y += 0.01
})
return <mesh ref={ref} />

A ref callback. For one-time setup when the object is created, a ref callback receives the object directly. Keep its identity stable with useCallback: React re-invokes an inline callback on every render. It runs again with a new object when args change.

// Braces matter: a React 19 ref callback's return value is its cleanup, and lookAt returns the mesh
const setup = useCallback((mesh: THREE.Mesh | null) => {
  mesh?.lookAt(0, 0, 0)
}, [])
return <mesh ref={setup} />

onUpdate is no longer reserved, so it is applied to the object by name like any other prop. That is deliberate for <texture>: three.js has a real Texture.onUpdate callback that the renderer fires after uploading the texture to the GPU, and <texture onUpdate={fn} /> now sets it. It is not called when props change, so a v9 onUpdate={(self) => (self.needsUpdate = true)} no longer flags a new image for upload: set needsUpdate in an effect keyed on the data instead. On a TSL node the prop replaces the Node.onUpdate() method, and on anything else (a mesh, a group) the assignment is inert. R3F logs a one-time notice the first time it sees an onUpdate prop.

Split Gain Map Environments Removed

<Environment>, useEnvironment and the Canvas background prop no longer read the split gain map format (['sdr.webp', 'gainmap.webp', 'metadata.json']), and @monogrid/gainmap-js is no longer a dependency. Use a single-file Ultra HDR .jpg, which three's own UltraHDRLoader decodes, or keep the split format through drei's <Environment>. .hdr, .exr and six-face cube sets are unchanged.

- <Canvas background={{ files: ['sdr.webp', 'gainmap.webp', 'metadata.json'] }} />
+ <Canvas background="/environment.jpg" />

<color attach="background"> Pattern Deprecated

The pattern of using <color attach="background"> to set the scene background is deprecated in favor of the new background prop on Canvas.

- <Canvas>
-   <color attach="background" args={['#1a1a2e']} />
-   <Scene />
- </Canvas>
+ <Canvas background="#1a1a2e">
+   <Scene />
+ </Canvas>

The new background prop is more powerful, supporting colors, URLs, presets, and expanded object form for separate background/environment maps:

// Simple color
<Canvas background="steelblue" />

// Hex number
<Canvas background={0x1a1a2e} />

// Environment preset (applies to both background and PBR lighting)
<Canvas background="sunset" />

// HDR file
<Canvas background="/path/to/environment.hdr" />

// Advanced: separate background and environment
<Canvas background={{
  preset: 'city',
  backgroundBlurriness: 0.5,
  environmentIntensity: 1.2,
}} />

The old <color attach="background"> pattern still works but is more verbose and doesn't support environments. See Canvas Background for full documentation.


What's New

Powerful New Scheduler

v10 completely rewrites the frame loop with a scheduler that gives you real control over how and when your code runs.

Your existing code works unchanged. But now you can do so much more:

Run code in the right order - No more guessing with priority numbers. Organize your frame work into logical phases that execute in sequence:

useFrame(({ delta }) => processInput(), { phase: 'input' })
useFrame(({ delta }) => physicsWorld.step(delta), { phase: 'physics' }) // fixed 1/60 steps
useFrame(({ delta }) => updateGame(delta), { phase: 'update' })
useFrame(({ delta }) => renderEffects(), { phase: 'render' })

Throttle expensive work - Heavy AI computation? Particle updates? Run them at whatever framerate makes sense:

useFrame(() => runAI(), { fps: 20 })
useFrame(() => updateParticles(), { fps: 30 })
useFrame(() => smoothAnimation()) // Full speed

Pause and resume anything - Every useFrame returns controls. Build pause menus, debug tools, or conditional animations:

const physics = useFrame(() => simulate(), { phase: 'physics' })
const animations = useFrame(() => animate())

// Pause physics but keep animations running
physics.pause()

// Resume when ready
physics.resume()

Multiple canvases, one loop - The scheduler manages all your Canvas instances in a single RAF loop. No more frame sync issues.

📚 Learn more: Frame Loop & Scheduler


WebGPU & TSL Hooks

v10 brings first-class WebGPU support with a suite of hooks that make TSL (Three.js Shading Language) feel native to React.

Write shaders the React way - Uniforms, nodes, and post-processing all managed declaratively. Share shader logic across components. Update values without re-renders.

import { Canvas } from '@react-three/fiber'
import { useUniforms, useNodes, useLocalNodes } from '@react-three/tsl'
import { Fn, vec3, sin, time, positionLocal, normalLocal } from 'three/tsl'

// Define reusable shader functions
function GlobalEffects() {
  useNodes(() => ({
    wobble: Fn(() => vec3(sin(time), sin(time.mul(1.3)), sin(time.mul(2)))),
  }))
  return null
}

// Use them anywhere - shared across your entire app
function WobblyMesh() {
  const { uIntensity } = useUniforms({ uIntensity: 0.5 })

  // Build the node once, outside JSX: a node created inline is new on every render and recompiles the shader
  const { positionNode } = useLocalNodes(
    ({ nodes }) => ({ positionNode: positionLocal.add(normalLocal.mul(nodes.wobble.mul(uIntensity))) }),
    [uIntensity],
  )

  return (
    <mesh>
      <sphereGeometry />
      <meshStandardNodeMaterial positionNode={positionNode} />
    </mesh>
  )
}

What you get:

  • useUniforms - Create and share uniforms across components. Scoping prevents naming conflicts. Auto-converts Leva controls.
  • useNodes - Define TSL functions once, use everywhere. Share expensive noise functions, effects, varyings.
  • useLocalNodes - Compose shared nodes with local logic. Access all uniforms and nodes from state. The second argument is a useMemo-style dependency array: [] reuses the result across renders, [a, b] rebuilds when a declared JavaScript input changes, and no array re-evaluates every render (a useCallback creator no longer opts out of that - pass [] instead).
  • useRenderPipeline - Declarative render pipeline setup with automatic MRT configuration. The component that configures it owns the pipeline and frees it on unmount ({ persist: true } to keep it); callbacks re-run after a hot update.

TSL hooks moved to @react-three/tsl

The TSL resource hooks live in their own package. Install it and change the import; apart from useRenderPipeline (below), they work as before. state.uniforms, state.nodes, state.buffers and state.gpuStorage stay on RootState (the package adds them), so useFrame(({ uniforms }) => ...) and useThree((s) => s.uniforms) keep working.

pnpm add @react-three/tsl@alpha
// Before
import { Canvas, useUniforms, useNodes } from '@react-three/fiber/webgpu'

// After
import { Canvas } from '@react-three/fiber'
import { useUniforms, useNodes } from '@react-three/tsl'

This covers useUniforms, useUniform, useNodes, useLocalNodes, useBuffers, useGPUStorage, useRenderPipeline, and the rebuildAll* helpers. In tests, import them from @react-three/tsl too: @react-three/test-renderer/webgpu no longer re-exports them.

useRenderPipeline now belongs to the component that configures it. When that component unmounts, its pipeline and scene pass are freed and the canvas renders without post-processing again; before, the pipeline kept rendering until reset(). Pass { persist: true } as the third argument to keep the old behaviour. A call without callbacks only reads the pipeline and no longer creates one. See Render Pipeline.

Deprecated standalone TSL utilities removed

removeUniforms(set, ...), clearScope, clearRootUniforms, removeNodes(set, ...), clearNodeScope and clearRootNodes are removed. Use the utilities the hooks return instead: const { removeUniforms, clearUniforms } = useUniforms() and const { removeNodes, clearNodes } = useNodes().

📚 Learn more: WebGPU Overview


TypeScript Changes

Hooks Follow the Import

useThree, useFrame, useRenderTarget and onCreated are typed for the renderer of the entry you import them from: WebGPURenderer on @react-three/fiber, WebGLRenderer on /legacy. The Register declaration earlier alphas suggested for this is no longer needed in apps. See Hooks Typed for WebGPU and Typing the renderer.

Test Renderer Entry Points

Match your test renderer import to your fiber import:

// If using default fiber (WebGPU: mocks WebGPU and creates WebGPU canvases)
import ReactThreeTestRenderer from '@react-three/test-renderer'

// If using legacy (WebGL)
import ReactThreeTestRenderer from '@react-three/test-renderer/legacy'

// Deprecated with @react-three/fiber/webgpu (removed in the first beta)
import ReactThreeTestRenderer from '@react-three/test-renderer/webgpu'

FAQ

Do I need to switch to WebGPU?

Not if your scene needs WebGL: import from @react-three/fiber/legacy, which is fully supported and keeps WebGLRenderer. But WebGPU is the default now, so staying on WebGL is the choice you make explicitly.

You don't need browser WebGPU support for the default either: WebGPURenderer falls back to a WebGL2 backend automatically, and keeps running node materials and TSL there.

Will my drei/postprocessing code work?

On @react-three/fiber/legacy, yes: it keeps the v9 WebGLRenderer surface these packages build on. On the default import, the renderer-agnostic parts of drei (controls, cameras, loaders, Html, ...) work, but anything built on GLSL or EffectComposer (most of postprocessing, drei's shader materials) needs /legacy until those packages support WebGPURenderer.

There is also a peer-range caveat during the alpha: a @react-three/drei release declaring support for @react-three/fiber@^10 is still pending, so your package manager may warn or refuse to resolve until it lands.

What if I'm using state.gl everywhere?

It still works: state.gl aliases state.renderer. On a WebGPU canvas you'll see a one-time deprecation notice in development, and since that renderer is a WebGPURenderer, WebGL-only calls on it fail; if your code makes those, import from /legacy. Update to state.renderer when convenient.


Migration from v9

If you're coming from v9, also review the v9 Migration Guide for React 19 compatibility changes that may apply.


Need Help?