Upon navigation in nextjs project triangles and shaders increase

Hello people,
still porting the globe to r3f but I am experience some issues.
You can find the repo here GitHub - lezan/next-globe, I extract a minimum reproduction repo with only nextjs 14 and r3f latest version.
If you run it and navigate from the home page to contact and then move back to home, you will see triangles and shaders increase as showed by r3f-perf.
The initial scene have 22178 triangles and 4 shaders (not sure where the 4th shader come from). Upon navigation we achieve 29656 and 10 shaders.
I tried to render each component (sphere.tsx, points.tsx and glow.tsx) by one one to see what is going on:

  • with sphere.tsx only, I have 9800 triangles and 2 shaders, then 9800 triangles and 4 shaders.
  • with glow.tsx only, I have 4900 triangles and 1 shaders, then 4900 triangles and 2 shaders
  • with points.tsx only, I have 7468 triangles and 1 shaders, then 14956 triangles and 4 shaders.

So the difference between 29k and 22k come from points.tsx. Shaders difference come from all components and they are always doubled.

If you see in globe.tsx I am using <Preload all /> component from drei. If I remove it things change a bit:

  • with sphere.tsx only, I have 9800 triangles and 2 shaders, then 9800 triangles and 2 shaders.
  • with glow.tsx only, I have 4900 triangles and 1 shaders, then 4900 triangles and 1 shaders
  • with points.tsx only, I have 7468 triangles and 1 shaders, then 14956 triangles and 2 shaders.

So in this case increase come only from points.tsx, both shaders and triangled doubled.

If I switch from <Link> component to <a> I do not see issues because of canvas unmounting (right?).

I am doing something wrong (missing disposal?), but I do not know what and how I can fix it. I also asked about it in discord (r3f one) and someone reply to me with stable reference to uniforms, creates a new object every render, but I tried to follow example and it appear good to me.

I’m not sure but I think r3fperf just counts up? If the scene is unmounted and a new scene is mounted I wouldn’t worry, there’s noting left behind nor can it double render.

I don’t see anything wrong in the code either. It’s fine. Just a little wasted code, for instance you don’t have to mess with event handlers and resize, this is all built in.

const { width, height } = useThree((state)=>state.size)

Is reactive. There’s also viewport, the same in three-units. You can delete all that, also the getViewsize helpers.

const { width, height } = useThree((state)=>state.viewport)
...
// this plane fills the screen
<mesh scale={[width, height, 1]}>
  <planeGeometry />

Hello Paul, thanks for your interest.

You mean I can avoid the use of getViewSize() function and just use const { width, height } = useThree();? Then remove the resize event handler and just use an useLayoutEffect() to calculate custom position and scale? Because I want to change position and scale according to window width. Something like

const [scale, setScale] = useState(0);
const [position, setPosition] = useState([0, 0, 0]);

const controls = useAnimationControls();

const { size } = useThree();

useLayoutEffect(() => {
  const getScale = () => {
    const scale = calculateScale(size.width, size.height);

    return scale;
  };

  const getPosition = () => {
    const { x, y } = calculatePosition(size.width, size.height);

    return [x, y, 0 ];
  };

  setScale(getScale());
  setPosition(getPosition());
}, [size.width, size.height]);

return (
  <group
    ref={positionParent}
    position={position}
  >
    <group ref={rotationParent} scale={scale}>
      <Suspense fallback={null}>
        <Sphere />
      </Suspense>
      <Suspense fallback={null}>
        <Points />
      </Suspense>
    </group>
  </group>
);

Then I will try to fix the animation in some way. I get it right?

Besides this, any input on the issue? Because if there are not left over, when I navigate back to home where canvas is, points.tsx is not rendered correctly. They are all positioned at the center of the globe, overlapped.

Thank you so much Paul!

useThree is reactive. if any value changes it will call the component. for instance when you resize the browser.

const { size } = useThree()
return (
  <group
    ref={positionParent}
    position={[...calculatePosition(size.width, size.height), 0]}>
    <group ref={rotationParent} scale={calculateScale(size.width, size.height)}>
      <Suspense fallback={null}>
        <Sphere />
      </Suspense>
      <Suspense fallback={null}>
        <Points />
      </Suspense>
    </group>
  </group>
)

but imo even that is too much code, you don’t need calculateScale and calculatePosition because you have all that info already. size gives you the size of the dom canvas in pixels, viewport gives you the threejs viewport which depends on the camera, you calculate that with getViewSizeAtDepth but viewport is already that.

it’s probably this, if i read your code correctly

const { viewport } = useThree()
return (
  <group
    ref={positionParent}
    position={[...LAYOUTPOS(viewport.width, viewport.height), 0]}>
    <group ref={rotationParent} scale={viewport.width * LAYOUTSCALE) / 200}>

ps, we already have a responsive flexbox implementation that has tailwind attributes GitHub - pmndrs/uikit: 🎨 user interfaces for react-three-fiber you can use that also to distribute generic content.

pps, i think you generally mix up effects and memoization. expensive calculations go into useMemo.

// only calculate once bar changes, otherwise memoize
const foo = useMemo(() => calculate(bar), [bar])

you don’t need to memoize for simple values calculations, this is called a derived value, like what my code does above. useEffect and useLayoutEffect are for side effects, or for accessing the created elements via their refs.

I have isolated it down to <Perf /> and this bit here: drei/src/core/Preload.tsx at 67c7cab2f80bbd7934c6e5864f37983e0ccc059a · pmndrs/drei · GitHub

This is more clear in this minimal example: https://codesandbox.io/p/sandbox/frosty-snow-fwnnj9?workspaceId=23740be7-92ae-41b3-8bce-d6b6b9f177fc

In the CSB, Perf reports 2 shaders even though there is only one. When the effect is commented out, PErf reports the correct number of shaders. Dispose behaviour is correct when visibility is toggled.

This is ether a reporting but in r3f-perf or some internal 3JS oddity with RenderTargets

most probably harmless build up. i don’t know from where it comes from, you could make an issue in their repo. im guessing though this is three since it caches materials and once registered it stays. doesn’t mean it takes any resources or render time if it isn’t used. if that’s the case it wouldn’t be a bug either, the purpose of a cache is to preempt usage.

im guessing though this is three since it caches materials and once registered it stays

There is only ever one shader in the scene, why would rendering to a render target (only for a single render call) cause it to double up?

Instead, you’d expect the render target draw to use the same cached shader no?

Even without r3f-perf, the number of programs in memory is reported by Three is incorrect in r3f: https://codesandbox.io/p/sandbox/inspiring-tesla-5ldqfz?workspaceId=23740be7-92ae-41b3-8bce-d6b6b9f177fc

interestingly, this issue does not occur in vanilla: https://codesandbox.io/p/sandbox/great-firefly-49s2qm

Getting back to you all with some updates.

Let’s focus before on my original issue, shaders and triangles upon navigation.
I followed what @smoking suggested and I switched from Drei’s shaderMaterial to manual <shaderMaterial />.
Two behaviors:

  • With <Preload all /> shaders double from 4 to 8.
  • Without <Preload all /> shaders stay at 4.
  • Triangles with or without stay at 22k.

First question: does it mean am doing something wrong with Drei’s shaderMaterial or is there a bug in shaderMaterial?
Second question: does <Preload all /> shows an issue in my scene or the behavior is intended or is bugged?
Third question: r3f-perf show 4 shaders in scene but I am actually using only 3 (also at loading, not only upon navigation). What is going on?

Meanwhile I am gonna try to include the suggested changes on position and scale manage.

Thank you so much to both for the help! So much appreciated, I’ve been banging my head about this problem since days.

It shows all shaders, user land and otherwise. You use something that created fourth. Also doesn’t mean it’s being used.

The fourth come from sphere.tsx, if I render only it I see 2 shaders. Could be the transparent props in <shaderMaterial />?

I can confirm that the shader come from the transparent. So that answer the third question. It would be cool to know about the first question because shaderMaterial from drei suits better.

About the useMemo/useEffect. I need to use an effect because I need to animate and make it conditional (animate only on load but not on resize). Unfortunately, I can’t find a better solution.