Performance issue with threejs on scrolling


Hello, I am a bit new to threejs and I’m trying to learn it by doing a new website for the student activity I’m enrolled in, but I’m having some performance issues with one model on that website.

In the video above you can see that I’ve got a 3d model of a rocket at the top of the page and it renders just fine, but when I scroll down and return back to it again, I found some garbage in the threejs canvas.
This problem only happens in mobile devices (Chrome mobile precisely as every other mobile browser I’ve tested like firefox - opera - Samsung internet - mi browser, etc… are behaving well)
This problem began to appear when I added a youtube iframe to the page where the rocket is located but before that, everything was fine.

This is the URL of the page where the rocket is located.

I’ve tested it on several smartphones ranging in performance level, and they all share the same problem on chrome mobile.

I don’t know how to solve such a problem and even I don’t know where to start looking so any help will be appreciated :heart:

Some information about the app that might be useful:
This website is using threejs 0.137.5, react-three/fiber 7.0.26, react-three/drei 18.10.1 with nextjs 12.1.0 and react 18.0.0 (Because of the suspense react server component)

Note that the website above is having a 3d model similar to the rocket in the video at the beginning of each page of it but none of these pages models’ had such a problem but the rocket.
The rocket glb file is about 93.1KB (which isn’t that large)

Thanks for reading all of this and for the help :heart:
Sorry if there were any English mistakes as it isn’t my first language.

without code there’s nothing much to do. it would also be useful to get console logs off mobile, perhaps even a webgl status report where you can see how and what crashed the canvas. on desktop where it doesnt happen the console is still littered with exceptions.

ps this is not the issue, but you’re sticking a scene into a mesh

pps. you’re running react 18 @ rc, but not for r3f. im guessing you have two react running side by side in that project. i don’t think that can hurt the canvas and how three/webgl renders, but still, i would rectify that first. either use react stable, or @rc for react and @beta for r3f: Building live envmaps RC2 - CodeSandbox (you can see the correct versions in there)

Thanks for the response, I just didn’t put the code as I thought it was a problem with chrome mobile but here you can find it below

As for the suggestion to use r3f @beta, I tried to use the same versions provided on the sandbox but the app crashed immediately with an error from the canvas component:

TypeError: Cannot create property ‘_updatedFibers’ on number ‘0’

If it’ll help you can take a look at the glb file here

const controlsRef = useRef();
const polarAngle = Math.PI / 2 - 0.2;

const Rocket = () => {
  const url = "/models/rocket.glb";
  const [model, setModel] = useState();
  const loadedModel = useLoader(GLTFLoader, url, (loader) => {
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath(
      "https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/libs/draco/"
    );
    loader.setDRACOLoader(dracoLoader);
  });

  const state = useThree();
  const logoMeshRef = useRef();

  const rocket = document.querySelector(".rocket.hero .drei");
  const initialScale =
    window.innerWidth < 980 && window.innerWidth >= 831 ? 1.9 : 1.4;
  const [scale, setScale] = useState(
    rocket.clientWidth < 325
      ? (rocket.clientWidth / 325) * initialScale
      : initialScale
  );

  useEffect(() => {
    window.addEventListener("resize", () => {
      state.setDpr(window.devicePixelRatio);
      initialScale =
        window.innerWidth < 980 && window.innerWidth >= 831 ? 1.9 : 1.4;
      setScale(
        rocket.clientWidth < 325 && window.innerWidth >= 540
          ? (rocket.clientWidth / 325) * initialScale
          : initialScale
      );
    });
    state.setDpr(window.devicePixelRatio);
    if (!model) {
      setModel(loadedModel);
    }

    import("dat.gui").then((dat) => {
      const gui = new dat.GUI();
      gui.destroy();
      gui
        .add(logoMeshRef.current.rotation, "x")
        .min(2 * -Math.PI)
        .max(2 * Math.PI)
        .step(0.01)
        .name("XRotation");
      gui
        .add(logoMeshRef.current.rotation, "y")
        .min(2 * -Math.PI)
        .max(2 * Math.PI)
        .step(0.01)
        .name("YRotation");
      gui
        .add(logoMeshRef.current.rotation, "z")
        .min(2 * -Math.PI)
        .max(2 * Math.PI)
        .step(0.01)
        .name("ZRotation");
    });
  }, []);

  if (model) {
    return (
      <>
        <ambientLight intensity={0.6} />
        <pointLight
          castShadow
          position={[0, 0, 4]}
          intensity={1}
          color={0x5ec8f6}
        />
        <pointLight
          castShadow
          position={[0, 2, -4]}
          intensity={0.5}
          color={0x5ec8f6}
        />
        <pointLight
          castShadow
          position={[4, 0, 0]}
          intensity={0.4}
          color={0x5ec8f6}
        />
        <pointLight
          castShadow
          position={[-4, -2, 0]}
          intensity={0.4}
          color={0x5ec8f6}
        />
        <mesh ref={logoMeshRef}>
          <primitive
            scale={[scale, scale, scale]}
            position={[0, -0.3, 0]}
            object={model.scene}
            dispose={null}
          ></primitive>
        </mesh>
        {/* <axesHelper /> */}
      </>
    );
  } else return null;
};

<Canvas>
  <OrbitControls
    ref={controlsRef}
    enablePan={false}
    enableZoom={false}
    rotateSpeed={0.5} 
    autoRotate
    autoRotateSpeed={0.9}
    maxPolarAngle={polarAngle}
    minPolarAngle={polarAngle}
  />
  <Suspense fallback={<Html><h4>Loading...</h4></Html>}>
     <Rocket />
  </Suspense>
</Canvas>

i would not mess with experimentals for now, use stable for everything, and definitively don’t mix.

the code above seems a little confused, you don’t need all the dpr stuff, draco decoding, it’s all taken care of automatically. i do not understand what you’re doing with setModel at all.

import { useGLTF } from '@react-three/drei'

function Rocket() {
  const { size } = useThree()
  const { scene } = useGLTF("/models/rocket.glb")
  const scale = size.width < 980 && size.height >= 831 ? 1.9 : 1.4
  return (
    <>
      ...
      <primitive object={scene} position={[0, -0.3, 0]} scale={scale} />

<Canvas dpr={[1, 2]}>
  <Suspense fallback={null}>
    <Rocket />

canvas will pick the right dpr, between 1 & 2, it prefers 2 if device dpr allows. useGLTF takes care of draco decompress. the scene is guaranteed to be present after the hook executes, you don’t need to setModel it or check its presence. size is reactive, when the container the canvas sits in changes it will fire and give you fresh bounds.

despite that, none of it will cause the canvas to fall out like that.

ps. if you have trouble scaling a model according to screen size, use viewport which gives you the scale in threejs units:

  const { viewport } = useThree()
  return (
    <mesh scale={[viewport.width, viewport.height, 1]}>
      <planeGeometry />

that would make the plane always fill the screen. you can now adjust these bounds, or position and scale objects accordingly.

1 Like

Thanks for all your suggestions, I really appreciate it :heart:
I will try what you said but for the size, I’m doing all of this mess because the canvas have a fixed height in css which breaks all the responsitivty that comes shipped by r3f by default, I gave the canvas a fixed height because before that it was taking much space
If there is a solution for this issue, I will appreciate it

Thanks for the help again :heart:

i dont see why it would have to be fixed. it should either be a percentage, in a grid or flex. then it will always stay responsive and fit into the whole.

I tried once before to make it in percentages but things got wrong on different screen sizes.
For now, I tried cleaning the code as you advice me above and it ended like this

import { useState, useEffect } from "react";
import { useGLTF } from "@react-three/drei";

const Rocket = () => {
  const { scene } = useGLTF("/models/rocket.glb");
  let initialScale =
    window.innerWidth < 980 && window.innerWidth >= 831 ? 1.9 : 1.4;
  const rocket = document.querySelector(".rocket.hero .drei");
  const [scale, setScale] = useState(
    rocket.clientWidth < 325
      ? (rocket.clientWidth / 325) * initialScale
      : initialScale
  );

  const resize = () => {
    initialScale =
      window.innerWidth < 980 && window.innerWidth >= 831 ? 1.9 : 1.4;
    setScale(
      rocket.clientWidth < 325 && window.innerWidth >= 540
        ? (rocket.clientWidth / 325) * initialScale
        : initialScale
    );
  };

  useEffect(() => {
    window.addEventListener("resize", resize);
    resize();
  }, []);

  const pointLights = [
    { intensity: 1, position: [0, 0, 4] },
    { intensity: 0.5, position: [0, 2, -4] },
    { intensity: 0.4, position: [4, 0, 0] },
    { intensity: 0.4, position: [-4, -2, 0] },
  ];

  return (
    <>
      <ambientLight intensity={0.6} />
      {pointLights.map((pointLight, i) => (
        <pointLight
          castShadow
          key={i}
          position={pointLight.position}
          intensity={pointLight.intensity}
          color={0x5ec8f6}
        />
      ))}
      <mesh>
        <primitive
          scale={[scale, scale, scale]}
          position={[0, -0.3, 0]}
          object={scene}
          dispose={null}
        ></primitive>
      </mesh>
    </>
  );
};

Much cleaner for sure, thanks very much for the advice :heart:
I managed to get the console logs from chrome mobile and after some debugging, I found that this error comes up whenever the canvas go crazy as before


And after searching for a bit, I’ve found that this can be caused by either a large memory leak or If the device has limited resources, The second option is rejected for my case as this problem persists on high-end smartphones.
So I think it’ll be a memory leak but I can’t find the source of it?
If you have any idea about finding such a leak that will be very helpful to me :pray:

I’ve also tried to reload the page on context lost with a webglcontextlost event listener as follows but that didn’t work

    rocket.addEventListener("webglcontextlost", (e) => {
        window.location.reload();
    });

I’m thinking of optimizing the 3d model (decreasing the number of polygons as an example) and I’ve already asked my mate working on the 3d models to do that, but do you have an idea whether this is gonna make a change or not? “I’m thinking that this will solve the problem a little bit but still on weaker devices the problem will still to presist”