R3F scenes that do not kill laptop batteries: a 2026 guide
After three «your site spins up my fans» complaints we rebuilt six React Three Fiber scenes. Power draw fell 4–7× without visible quality loss.
Creastra Digest
- The main culprit is endless requestAnimationFrame; switch to frameloop="demand"
- Cap DPR at 1.5 instead of Math.min(window.devicePixelRatio, 2)
- IntersectionObserver pauses out-of-viewport scenes — that alone takes 60% off
In autumn 2025 we got three complaints in a row: «I open your case study and my laptop's fans hit the ceiling». All three were M2 MacBooks with integrated graphics. Our studio home and three client projects ran R3F scenes — product morph, background particles, interactive hero. I checked Activity Monitor honestly: 28% CPU and 41% GPU on an idle page. We rebuilt six scenes. Here is how.
Root cause — endless rAF
By default R3F runs requestAnimationFrame at 60 FPS even when nothing on the scene moves. That is the biggest drain on modern integrated GPUs. One-line fix: set Canvas frameloop="demand" and call invalidate() manually on change.
// frameloop="demand" — a frame renders only on invalidate()
<Canvas frameloop="demand" dpr={[1, 1.5]}>
<Scene />
</Canvas>
// inside the scene — animate only on something meaningful
function Box() {
const ref = useRef<Mesh>(null!);
const { invalidate } = useThree();
useFrame((_, dt) => {
if (!hovered) return;
ref.current.rotation.y += dt;
invalidate(); // keep frames coming while hovered
});
return <mesh ref={ref} />;
}DPR and pixel density
The classic mistake: dpr={Math.min(window.devicePixelRatio, 2)}. On retina that is 2 and you render 4× more pixels than the eye can resolve. We cap at 1.5 — invisible on retina, and on FullHD we are already at 1.0. Power draw nearly halved.
IntersectionObserver — pause off-screen
- If a scene sits below the fold, it renders in the background by default
- Wrap the Canvas in an observer and disable frameloop when invisible
- Re-enable on visibility with an 80 ms debounce to avoid scroll jitter
- Same trick for scenes inside tabs — render only the active one
- On a 4-scene page this alone removes 60% of total consumption
Geometry and materials
A 180k-triangle GLB on a webpage is almost always overkill. Run it through gltfpack or Blender Decimate down to 35–60k — invisible at 600×400. Replace PBR materials with MeshStandardMaterial without envMap where reflections do not matter. Shadow casting only on the hero object; bake the rest.
Postprocessing — the biggest temptation
Bloom, depth-of-field, SSAO look gorgeous in dev on a desktop with discrete graphics. On an unplugged M2 they turn the laptop into a toaster. Rule: enable postprocessing only when prefers-reduced-motion = no-preference, navigator.deviceMemory >= 8, and not on battery (via navigator.getBattery).
Checklist «do not burn the laptop»
- frameloop="demand" by default
- DPR capped at 1.5
- IntersectionObserver on every Canvas
- Geometry trimmed to target triangle count
- No postprocessing on battery
- Shadows only on one scene object
- Lights — at most three, everything else baked
- prefers-reduced-motion falls back to a static poster