Performance drops at higher resolutions

Hi there,

I’m relatively new to the whole three.js fun and hope someone has patience to help here :slight_smile:

I used a relatively dirty sketch and tutorials to put my site together, the good thing is it works, the bad thing is the site has performance issues. Every time you scroll down and the scene is no longer visible, the page starts lagging. There is an FPS drop.

At first I thought it was the useFrame function, because the page was lagging on my 144Hz / FPS monitor and I had the assumption that the 144 FPS on the scene was the cause. But Hz / FPS are less the problem than the resolution. The performance problems only really emerge with resolutions above 1080p.

I was looking for an option with a frame limiter, later I went to IntersectionObserver to stop rendering the scene when it is not visible. The observing works, but I couldn’t get a stop render code there (shame on me).I got the code snippet from here.

Can someone help me there how to get a stop render code there? Thanks in advance!

1 Like

There’s the option to simply use a boolean that switches on change of intersection observer, for instance if scroll top is more than the height and offset top of your three container a boolean isInView could be set to false then used in the following way…

const animate = () =>{

if(isInView){
 render() 
} 

 window.requestAnimationFrame(animate) 
} 

const render = () =>{
 renderer.render(scene,camera)
} 

You could also setup logic with this boolean to cancel animation frame and request it again when needed…

1 Like

Hello, thank you very much for your tip. I have already seen and tried this solution. But unfortunately it doesn’t work, regardless if the scene is in the viewport or not, it is rendered all the time, although with the IntersectionObserver the different divs are recognized…

I investigated further and here are the results based on the resolution that is displayed. The performance problems occur at HD or higher because the scene has to render much more. Is there a culprit here in my code?

import * as THREE from "three"
import { useRef } from "react"
import { Canvas, useFrame } from "@react-three/fiber"
import { EffectComposer, SSAO } from "@react-three/postprocessing"
import { BallCollider, Physics, RigidBody, CylinderCollider } from "@react-three/rapier"

THREE.ColorManagement.legacyMode = false

const baubleMaterial = new THREE.MeshPhysicalMaterial({
  roughness: 0.65,
  // color: "#000000",
  // emissive: "#ffffff",  
  // transmission: 0.5, // Add transparency
  // thickness: 1.0, // Add refraction!
});
const sphereGeometry = new THREE.IcosahedronGeometry(1, 0)
const baubles = [...Array(40)].map(() => ({ scale: [0.75, 0.75, 1, 1, 1.25][Math.floor(Math.random() * 5)] }))

function Bauble({ vec = new THREE.Vector3(), scale, r = THREE.MathUtils.randFloatSpread }) {
  const api = useRef()
  useFrame((state, delta) => {
    delta = Math.min(0.1, delta)
    api.current.applyImpulse(
      vec
        .copy(api.current.translation())
        .normalize()
        .multiply({ x: -20 * delta * scale, y: -100 * delta * scale, z: -50 * delta * scale }),
    )
  })
  return (
    <RigidBody linearDamping={0.75} angularDamping={0.9} friction={0.2} position={[r(20), r(20) - 25, r(20) - 10]} ref={api} colliders={false} dispose={null}>
      <BallCollider args={[scale]} />
      <CylinderCollider rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 1.2 * scale]} args={[0.15 * scale, 0.275 * scale]} />
      <mesh castShadow receiveShadow scale={scale} geometry={sphereGeometry} material={baubleMaterial} />
    </RigidBody>
  )
}

function Pointer({ vec = new THREE.Vector3() }) {
  const ref = useRef()
  useFrame(({ mouse, viewport }) => {
    vec.lerp({ x: (mouse.x * viewport.width) / 2, y: (mouse.y * viewport.height) / 2, z: 0 }, 0.2)
    ref.current.setNextKinematicTranslation(vec)
  })
  return (
    <RigidBody position={[100, 100, 100]} type="kinematicPosition" colliders={false} ref={ref}>
      <BallCollider args={[2]} />
    </RigidBody>
  )
}

const animate = () =>{

  if(isInView){
   render() 
  } 
  
   window.requestAnimationFrame(animate) 
  } 
  
  const render = () =>{
   renderer.render(scene,camera)
  } 

export const App = () => (
  <Canvas
    shadows
    gl={{ alpha: true, stencil: false, depth: false, antialias: false }}
    camera={{ position: [0, 0, 20], fov: 32.5, near: 1, far: 100 }}
    onCreated={(state) => (state.gl.toneMappingExposure = 1.5)}>
    <ambientLight intensity={0.5} />
    <spotLight position={[20, 20, 25]} penumbra={1} angle={0.2} color="white" castShadow shadow-mapSize={[512, 512]} />
    <directionalLight position={[0, 5, -4]} intensity={4} />
    <directionalLight position={[0, -15, -0]} intensity={4} color="#000000" />
    <Physics gravity={[0, 0, 0]}>
      <Pointer />
      {baubles.map((props, i) => <Bauble key={i} {...props} />) /* prettier-ignore */}
    </Physics>
    <EffectComposer multisampling={0}>
      <SSAO samples={11} radius={0.15} intensity={20} luminanceInfluence={0.6} color="#000000" />
      <SSAO samples={21} radius={0.03} intensity={15} luminanceInfluence={0.6} color="#000000" />
    </EffectComposer>
  </Canvas>
)

MeshPhysicalMaterial is the most expensive material three.js offers, and that cost is per pixel drawn. It might be worth comparing with the cheapest material (MeshBasicMaterial) to see if that explains the performance problems. Sometimes it’s OK to draw at lower resolution or DPI than your display allows, too.

1 Like

Hello, Thank you for your suggestion. I have tried it with all materials from the threejs docs, the difference in performance is minimal (75-90% GPU utilization depending on which material is used, but it never goes below 75%). The site also lags with the MeshBasicMaterial.

In the case of the code you’ve shared, being based in an r3f setup, creating a render loop is going to cause more harm than good as r3f is setup to handle rendering internally with useFrame and fwiu should also prevent rendering when a three canvas is outside of the viewport, are you running this locally or as a hosted environment?

Hello, thank you for your reply. I have found the culprit in the code which is responsible for the performance degradation:

    <EffectComposer multisampling={0}>
      <SSAO samples={11} radius={0.15} intensity={20} luminanceInfluence={0.6} color="#000000" />
      <SSAO samples={21} radius={0.03} intensity={15} luminanceInfluence={0.6} color="#000000" />
    </EffectComposer>

Postprocessing scales the required performance depending on the resolution. When I take it out, there are no more lags and the required performance of the graphics card remains at around 20%. But I don’t have nice and realistic shadows on the objects anymore…

If anyone has any ideas on how to reduce the power needed for post-processing effects, suggestions are very welcome. Otherwise, the performance problems of the website are solved for the time being. Thanks for your help!

What was the reason you were using 2 ao passes? You should now be able to use N8AO just very recently updated

With only one ao pass, some grain and noise is still visible, therefore 2x… (I know, not the correct way)

But thanks for the new suggestion. I’ve done some reading and N8AO would indeed be a resource-efficient alternative for post-processing applications. I will try to include it in my project. But for now I am satisfied that the performance problems are gone :slight_smile:

Thanks for the support!

1 Like