This is the best overview I could find: https://discoverthreejs.com/tips-and-tricks
The most important is gotcha in Threejs is that creating objects can be expensive, think twice before you mount/unmnount things! Every material that you put into the scene has to compile, every geometry you create will be processed. Share materials and geometries if you can, either in global scope or locally:
const geom = useMemo(() => new BoxBufferGeometry(), [])
const mat = useMemo(() => new MeshBasicMaterial(), [])
return items.map(i => <mesh geometry={geom} material={mat} ...
Try to use instancing as much as you can when you need to display many objects of a similar type!
const [x, setX] = useState(0)
useFrame(() => setX(x => x + 0.01))
// Or, just as bad ...
// useEffect(() => void setInterval(() => setX(x => x + 0.01), 30), [])
return <mesh position-x={x} />
You are forcing a full component (+ its children) through React and its diffing mechanism 60 times per second.
✅ Instead, use refs and mutate! This is totally fine and that's how you would do it in plain Threejs as well.
const ref = useRef()
useFrame(() => ref.current.position.x += 0.01)
return <mesh ref={ref} />
Instead use animation libs that animate outside of React! Avoid libs like react-motion that re-render the component 60fps!
✅ Using lerp + useFrame:
import lerp from 'lerp'
function Signal({ active }) {
const ref = useRef()
useFrame(() => ref.current.position.x = lerp(ref.current.position.x, active ? 100 : 0, 0.1))
return <mesh ref={ref} />
✅ Or react-spring, which animates outside of React:
import { a, useSpring } from 'react-spring/three'
function Signal({ active }) {
const { x } = useSpring({ x: active ? 100 : 0 })
return <a.mesh position-x={x} />
Using state-managers and selected state is fine, but not for updates that happen rapidly!
import { useSelector } from 'react-redux'
// Assuming that x gets animated inside the store 60fps
const x = useSelector(state => state.x)
return <mesh position-x={x} />
✅ Fetch state directly, for instance using zustand:
useFrame(() => ref.current.position.x = api.getState().x)
return <mesh ref={ref} />
✅ Or, subscribe to your state in a way that doesn't re-render the component:
const ref = useRef()
useEffect(() => api.subscribe(x => ref.current.position.x = x, state => state.x), [])
return <mesh ref={ref} />
In Threejs it is very common to not re-mount at all, see the "disposing of things" section in discover-three. This is because materials get re-compiled, etc.
Switch React to @experimental
and flag the canvas as concurrent. Now React will schedule and defer expensive operations. You don't need to do anything else, but you can play around with the experimental scheduler and see if marking ops with a lesser priority makes a difference.
<Canvas concurrent />