Scaling performance
This is a short primer on how to scale performance.
Websites like Sketchfab make sure the scene is always fluid, running at 60 fps, and responsive, no matter which device is being used or how demanding the loaded model is, which depends on the vertex count, draw calls, and so on.
They do this by scaling performance:
- Nested loading, lesser textures and models are loaded first, high-resolution later
- Movement regression, where effects, textures, shadows will slightly reduce until still-stand
In three-fiber both are easy to achieve.
Nested loading
The following sandbox goes through three loading stages:
- A loading indicator
- Low quality
- High quality
And this is how easy it is to achieve it, you can nest supsense and even use it as a fallback:
function App() {
return (
<Suspense fallback={<span>loading...</span>}>
<Canvas>
<Suspense fallback={<Model url="/low-quality.glb" />}>
<Model url="/high-quality.glb" />
</Suspense>
</Canvas>
</Suspense>
)
}
function Model({ url }) {
const { scene } = useGLTF(url)
return <primitive object={scene} />
}
Movement regression
As of version 6 three-fiber allows you to compose performance scaling.
The following sandbox uses expensive lights and post-processing. In order for it to run relatively smooth it will scale the pixel ratio on movement and also skip heavy post-processing effects like ambient occlusion.
When you inspect the state model you will notice an object called performance
.
performance: {
current: 1,
min: 0.1,
max: 1,
debounce: 200,
regress: () => void,
},
- current: Performance factor alternates between min and max
- min: Performance lower bound (should be less than 1)
- max: Performance upper bound (no higher than 1)
- debounce: Debounce timeout until it goes to upper bound (1) again
- regress(): Function that temporarily regresses performance
You can define defaults like so:
<Canvas performance={{ min: 0.5 }}>...</Canvas>
This is how you can put the system into regression
The only thing you have to do is call regress()
. When exactly you do that, that is up to you, but it could be when when the mouse moves, or the scene is moving, for instance when controls fire their change-event.
Say you are using controls, then the following code puts the system in regress when they are active:
const regress = useThree((state) => state.performance.regress)
useEffect(() => {
controls.current?.addEventListener('change', regress)
This is how you can respond to it
Your app has to opt into performance scaling by listening to the performance current
! The number itself will tell you what to do. 1 (max) means everything is ok, the default. Less than 1 (min) means a regression is requested and the number itself tells you how far you should go when scaling down.
For instance, you could simply multiply current
with the pixelratio to cut down on resolution. If you have defined min: 0.5
that would mean it will half the resolution for at least 200ms (delay) when regress is called. It can be used for anything else, too: switching off lights when current < 1
, using lower-res textures, skip post-processing effects, etc. You could of course also animate/lerp these changes.
Here is a small prototype component that scales the pixel ratio:
function AdaptivePixelRatio() {
const current = useThree((state) => state.performance.current)
const setPixelRatio = useThree((state) => state.setPixelRatio)
useEffect(() => {
setPixelRatio(window.devicePixelRatio * current)
}, [current])
return null
}
Drop this component into the scene, combine it with the code above that calls regress()
, and you have adaptive resolution:
<AdaptivePixelRatio />
There are pre-made components for this already in the drei library.