Babylon - NextJS lag on loading model

Hi

Premise:

I am lazy loading in my SSR (next.js) site a component that uses babylonjs (and react-babylonjs).

To lazy load I tried both next.js dynamic component

The component gets lazy loaded as soon as you scroll into that part of the page, leveraging Lazy load from: react-lazyload - npm

Reason:

I want to avoid loading beforehand the whole babylonjs library and the 3d model files (to leverage good pagespeed within the SEO world).

Problems:

  1. When I scroll, so lazy loadding triggers, the whole babylonjs’s library (serveral megabytes…) gets imported, tree shaking gets completely ignored and the page stutters as is a gigantic payload.
  2. When the model (a single model made of 4 meshes) is being downloaded (I used react-babylonjs but also I did it imperatively with ImportMeshAsync) the thread seems to be blocked and the page is unresponsive.

What I tried:

  1. I tried to use both and babylonjs’ native ImportMeshAsync to no use, the page blocks and is interactive just when the model is loaded
  2. I tried to rather than importing the destructured methods from @babylonjs/core importing them cherrypicking like: @babylonjs/core/Math/math but still the whole lib gets imported….

What I want to achieve:

  1. Page not being blocked so model to REALLY load async
  2. Lazy loading the tree shaked chunks of babylonjs not the whole lib….
import '@babylonjs/loaders/glTF';

import { useEffect, useRef, useState } from 'react';
import { Engine, Scene } from 'react-babylonjs';
import { Color4, Constants, HDRCubeTexture } from '@babylonjs/core';
import { Vector3 } from '@babylonjs/core/Maths/math';

import ModelViewer from '../ModelViewer';

const ModelViewerScene = ({ model }) => {
  const [isModelLoaded, setIsModelLoaded] = useState(false);
  const glowRef = useRef(null);
  const canvasRef = useRef(null);

  useEffect(() => {
    const handleDisableScrollOnCanvas = (event) => {
      event.preventDefault();
    };

    if (canvasRef.current.canvas) {
      canvasRef.current.canvas.addEventListener('wheel', handleDisableScrollOnCanvas);

      return () =>
        canvasRef?.current?.canvas.removeEventListener('wheel', handleDisableScrollOnCanvas);
    }
  }, [canvasRef]);

  const handleInitScene = ({ scene }) => {
    const hdrTexture = new HDRCubeTexture(
      './../game/environment/studio_small_09_1k.hdr',
      scene,
      256,
      false,
      true,
      false,
      false,
    );

    scene.environmentTexture = hdrTexture;
    scene.imageProcessingConfiguration.contrast = 1.6;
    scene.imageProcessingConfiguration.exposure = 0.6;
    scene.imageProcessingConfiguration.toneMappingEnabled = true;
    scene.clearColor = new Color4(0, 0, 0, 0.01);
  };


const handleOnModelLoaded = (e) => {
    console.log(e);
    setIsModelLoaded(true);
  };

  const canvasId = isModelLoaded ? 'nft-card-model--loaded' : 'nft-card-model';

  return (
    <Engine antialias adaptToDeviceRatio canvasId={canvasId} ref={canvasRef}>
      <Scene onSceneMount={handleInitScene}>
        <arcRotateCamera
          name="Camera"
          alpha={2}
          beta={1.5}
          radius={0}
          lowerRadiusLimit={20}
          upperRadiusLimit={80}
          target={Vector3.Zero()}
          useAutoRotationBehavior
          position={new Vector3(11, 0, -30)}
        />
        <hemisphericLight name="light1" intensity={3} direction={Vector3.Up()} />
        <glowLayer
          ref={glowRef}
          intensity={1}
          name="glow"
          options={{
            mainTextureSamples: 2,
            alphaBlendingMode: Constants.ALPHA_ONEONE_ONEONE,
          }}
        />
        {model && <ModelViewer handleOnModelLoaded={handleOnModelLoaded} sceneFilename={model} />}
      </Scene>
    </Engine>
  );
};

export default ModelViewerScene;

Any kind of help is appreciated

To your first point - if the model is large, there will be some synchronous computation involved (reading the vertex data). Afterwards it should continue async-loading. So the page might be blocked for a few moments, especially if the model is very complex. You can try separating the model to a few smaller ones, but I don’t know what and how you are loading it.
To your second point - To achieve tree shaking you will need to load all from the respective directory and load all side-effect files needed. It is documented here - https://doc.babylonjs.com/divingDeeper/developWithBjs/treeShaking

I don’t know how react-babylonjs loads the framework. This is a question from @brianzinn , but I assume it is built with tree shaking in mind. Avoid loading anything from the package root, load only from the right path.

React babylonjs uses codegen which creates components for you. The downside is that it brings in most of babylon core. react-babylonjs/generatedCode.ts at master · brianzinn/react-babylonjs · GitHub

You can preload pages or lazy components to fix anything that isnt immediately in the viewport. For me, static react-babylonjs apps feel smooth.

hi, if you are using react-babylonjs and trying to achieve a high level of tree-shaking then you will be disappointed. all of the types from that library are excluded from that bundle, but the code is generated for each supported class for diffing. that will be fixed in v4 either with dynamic registration via side-effects or having no generated code and losing lots of functionality. There are open issues on the repo and the implementation details are still up in the air. The underlying issue is no way to inspect the scene graph from JSX to tree shake (the renderer uses host elements - not Components).

If tree-shaking is important then use babylonjs-hook npm (not declarative):
babylonjs-hook - npm (npmjs.com)

1 Like

This next.js dynamic component works, here is an example (click Start Now it will load Babylon, but does not block UI).

The trick to prevent blocking UI when scrolling, is to use a Next.js Preload component, put it somewhere visible on the page before the component requiring babylon, at least a pagefold, so it will preload all resources before users even scroll near the actual component, like this:

/**
 * Preload Script for given Route without rendering (only works in Next.js production)
 * @param {String} to - pathname of the route to preload scripts for
 * @param {Object} [props] - other next Link options
 */
export function Preload ({to, ...props}) {
  // Next Link expects a native element with ref, and it has to render as HTML element to trigger preload
  return <NextLink href={to} {...props}><i className="hidden-preloader"/></NextLink>
}

You need to make sure you always import directly for all classes, if just one import comes from @babylonj/core the entire lib may get imported. Example:

/* Maths */
export { Color3, Color4, } from '@babylonjs/core/Maths/math.color'
export { Plane, } from '@babylonjs/core/Maths/math.plane'
export { Path3D, } from '@babylonjs/core/Maths/math.path'

Put bundle analyzer in your next.js build to verify, like so:

Like mentioned above, react-babylonjs is no good for tree shaking, I know this from exp. I wrote all wrapper react Components manually for Babylon stuff.

Another option could be to just split the codegen into multiple files. I would prefer that, personally.

Splitting the codegen until multiple files won’t help with tree-shaking afaik. The issue is those references are all needed because the way the renderer is implemented it needs to be able to handle all of the supported API without knowing what host elements will be in the tree graph. i am considering completely dynamic and losing support for custom handlers, but that is with a redo on diffing and object construction.

I recommend the dynamic import as well - there are a few threads here on that.

Hi guys, I am working together with OT on this project.

Preloading or loading beforehand is not an option as we want to have a good pagespeed insights score and it will be considered “unused javascript”.

I will try to refactor imperactively so that I avoid loading the whole lib and see what happens as as far as I understood is react-babylonjs because of its nature bundling the whole lib (becaus of codegen).

It’s not bundling the whole lib. It’s still tree shaking. I have added some imports for side effects to make it easier to use, but those will be removed in v4 s as well. You can run a bundle analyzer in next. It’s actually not a large bundle compared to the size of many assets. HTH.

Check out this guide on optimizing webpage speed with 3D configurators - there are benchmarks in the article proving “A” grade with 93% score.

1 Like

Thank you I will use some of that setup indeed!