brianzinn / react-babylonjs

React for Babylon 3D engine
https://brianzinn.github.io/react-babylonjs/
809 stars 102 forks source link

Is there any way to cache the mesh of a component ? #275

Closed Xample closed 1 year ago

Xample commented 1 year ago

Let's consider the following component

  function Board() {

    const [y, setY] = useState(0);

    useBeforeRender((scene) => {
      setY((y) => y + 1);
    });

    return (
      <>
          <transformNode
            name={`transform`}
            position={new Vector3(0, y , 0)}>
            <Stripe count={100000}></Stripe>
          </transformNode>
      </>
    );
  }

Where Stripe is very slow to redraw because it holds 100'000 items and therefore create a complex mesh.

When I change a prop of the transformNode, I do not expect Stripe to be redrawn, but it is. Is there any way to cache the result of Stripe if none of its direct props is changed ?

PS: as a workaround, I thought of using from-instance, but I then lose the benefits of having a declarative scene.

Xample commented 1 year ago

I remember to have seen a pureFunction prop (isPure) in the code. Would it be its purpose to cache the result if no props is changed ?

[EDIT] nope, isPure is part of BablonJS, not react https://forum.babylonjs.com/t/ispure-field-on-transformnode-class/17230

brianzinn commented 1 year ago

Can you share the Stripe component? If it's memoized then it shouldn't impact performance to have a parent component updated - or otherwise how is your Stripe component designed to be re-rendered?

Xample commented 1 year ago

sure here is the full code (range is taken from lodash and returns an array of [0,1,...,n])

If I put a breakpoint, the return <>... code stops at each frame.

  function Stripe(props: { length: number; count: number }) {
    const height = props.length / props.count;

    return (
      <>
        {range(props.count).map((x, i) => (
          <box
            name={`box-${i}`}
            key={i}
            width={1}
            height={height}
            depth={1}
          >
            <standardMaterial
              name={`mat-${i}`}
              diffuseColor={i % 2 ? Color3.Red() : Color3.Green()}
              specularColor={Color3.Black()}
            />
          </box>
        ))}
      </>
    );
  }
Xample commented 1 year ago

Note that if I use the lodash memoize to prevent going into that loop, like:

///only as a POC
const StripeMemoized = memoize(Stripe, (...args) => 1);

The scene is rendered as expected (very fast)

brianzinn commented 1 year ago

Looks resolved - just reopen if you have more questions.

Xample commented 1 year ago

Hello, actually using the memoize of lodash works, but it's a hack. What I'm wondering is how the caching of React should work in this situation. What I mean is if no property is changed of and there is no hook, neither should normally the rendering be done by design again ? Could it be related to the loop ?

brianzinn commented 1 year ago

i don't see a hack. https://beta.reactjs.org/reference/react/memo memo is your way to indicate a re-render isn't necessary unless props have changed. for example, if count changes then you want to re-render. Also, from looking at that if count hasn't changed then I don't think re-rendering would have an adverse impact since the keys would match. range looks the same as `Array.from(new Array(count), (...) => {}), but suspect the numbers remain unchanged. It looks like you are rendering a bunch of boxes on top of each other - not sure exactly what that is meant to be either - I use those kinds of loops all the time. There is an example here: https://brianzinn.github.io/create-react-app-typescript-babylonjs/

I call setState when the font is loaded and the ball will keep bouncing, so the page is rerendered, but the mesh is not disposed and re-created because of the key.

Xample commented 1 year ago

okay, as memo is the official React way, this is what I was looking for, thank you for pointing out this helper. I'm really new to react, I need to understand how bulding / rebuilding the rendering tree is done under the hood.

According to: https://www.developerway.com/posts/react-re-renders-guide#part2.5 there is no caching even if there is no property change, which I wrongly assumed.

I also understood that we can scope the hooks to an element by creating a function component for this element. I made the mistake of using a hook for children (a directional light just to name it) which had the side effect of re-rendering all the function-component's parent scope of that children.

your lib is really cool to use btw 👍🏻

brianzinn commented 1 year ago

oh cool! Glad that it's working out for you then. when it comes to performance issues with re-rendering sometimes we need to provide a bit of extra code to help out React!