Optimizing filesizes (different approaches!)

Hello! I’m pondering which approach to take when using Babylon on a project using Rollup, ES6 imports, and SSR / CSR rendering (SvelteKit).

My initial build has Babylon at 10MB, viewed via rollup-visualiser, which loads on each page, and can cause some 20 second delay between page navigation, not to mention everything else :sweat_smile:

Here’s my thinking:

Precise ES6 Imports (custom)

Since this will import the entire @babylonjs/core library:

import { ArcRotateCamera } from '@babylonjs/core'

Instead we specify precise locations into custom file:

export { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera.js'
export { Vector2, Vector3, Vector4 } from '@babylonjs/core/Maths/math.vector'

Pros

  • you can fully control exactly what bits go into the final build
  • there is a single myBabylonLibrary.js for ease of use

Cons

  • it’s not clear where each class is located in the directories, and LSP / autocomplete doesn’t know either
  • the rollup minified version doesn’t seem to be as small as official distribution minified version
  • the library gets entangled into the page load (so it tries to load between basic SSR / CSR page navigation, without there being some modifications around this)

Distribution Version (globalThis)

Classic minified version is simplest, with something like this:

<script src="https://cdnjs.cloudflare.com/ajax/libs/babylonjs/7.4.0/babylon.js" onload={onLoad}></script>

// elsewhere...

function onLoad() {
  globalThis.babylonInited = true
}

// elsewhere...

if (globalThis.babylonInited) {

  const { ArcRotateCamera, Vector2, Vector3, Vector4 } = globalThis.BABYLON

  // do stuff...

}

Alternatively using dynamic ES6 import:

function pageIsReadyAndLoaded() {
  const lib = import('https://cdnjs.cloudflare.com/ajax/libs/babylonjs/7.4.0/babylon.js')
  // etc...
}

Pros

  • preprepared and minified libraries which have the best filesize
  • lazy-loaded only when its needed and when the page is ready

Cons

  • can’t access anything Babylon until entire library is ready (see constants / enums)
  • doesn’t seem as small as it could be by comparison to official minified dist version
  • breaks ES6 / rollup workflow with LSP

Both Worlds (cake)

An ideal situation might be best of both worlds:

  • Import various basic classes (Vector3) and constants like Engine.ALPHA_MULTIPLY into rollup project freely, which have little overhead for page loads
  • A custom myBabylonLibrary.js can then be lazy loaded as it’s needed

Caveats

  • Still not sure where to find each class / enum / constant within the library, and:
  • Can enums and constants be imported separately?

Thanks!

2 Likes

cc @RaananW

Oh, we can have a very long discussion on that :slight_smile:

I am very biased towards the first approach. I completely understand each of your CONS, and you are totally right. Especially regarding the first point. I believe minification can be achieved better if configured better, and I believe that if exposed correctly, the first approach can behave like the second in terms of asyc-availability. However, the first point is “unsolvable”:

Since I know the framework quite well, I learned to overcome this problem. However, this is a serious issue for someone who just started using the framework. A simpler approach would be to provide a single importable file including all modules and let your bundler tree-shake it to get the minimum from what you need. There are many reasons why this approach does not (YET!!) work with Babylon. I have been trying to find the sweet spot between backwards compatibility and ESM-ability for quite some time (and have been talking about it publicly as well). It is not quite simple. I have made progress during 7.0 towards this goal - engine was separated into importable functions, side-effects were removed, which in turn allows us to bundle everything in a single file. The problem was - how do you rebuild the UMD version using these functions. Since we can’t suddenly tell half of our userbase - “hey, sorry, there is no Engine class anymore”, I had to find a way. And I did, but there was a performance hit of 3-5% on the UMD version, which is simply unacceptable.
I can on about this, as I invested quite some time learning and understanding the systems involved in that. What I can say is this - we are working on a solution. It might not be the best or most acceptable solution for everyone, because you sadly can’t keep everyone happy. But we are working on a solution :slight_smile:

To achieve this we will need to create a single file, side-effects-free, with a collection of classes and functions that can be imported when you need them. Either static imported or dynamically imported, both should work.
Regarding discoverability - this will always be an issue with such a large framework, doesn’t matter how we really pack it. Even in a single large file, as a developer with less experience with Babylon, you will need to follow the documentation in order to find the class you actually need. Even if they all come from the same file. Some would even argue it is not the best to have a list of 100s of classes in a single import (see the playground’s BABYLON namespace :slight_smile: ). So documentation is key.

My TL;dr is - option one can be improved to support lazy loading using dynamic imports, and this is the path i would always choose, with the current babylon architecture. VS Code does allow me to find the right file to import, and the documentation page(s) regarding ES6 support can help with finding side effects that are needed to support my development.

And a small note - the way I like to achieve this is to first import everything from @babylonjs/core . After I am happy with the result I remove all imports and start importing the classes and modules directly from their files one by one. Afterwards I am running tests, visualization tests, and manually test the app, and add side-effect files that are needed for the project to work. I dynamically import any module that is not needed for the first frame to get the fastest first-frame. This optimization step is a bit time consuming, but it is very rewarding in terms of what you deliver your users.

1 Like

Hi @RaananW - sorry for the late reply and thanks so much for your response! I can empathise making everything to be ESM-able must’ve been a pretty unenviable task - so happy to report the whole process once I got going was pretty much painless and smooth (hats off!) :partying_face:

In the end I created two files - a lib.preload.js which contains constants and common math classes that are used across GUI, and an lib.babylon.js containing the rest. Folder structure and naming conventions were really clear in the codebase - and I got autocomplete working using SublimeCodeIntel (for some reason LSP wouldn’t do) + adding ./node_modules/@babylonjs/* to jsconfig.json

Process was as you said - remove imports to the custom library bit by bit - and then once final @babylonjs/core import is removed there are a few side effects from missing libs. This was the only difficulty and handled by importing back in Helpers and Extensions!

Total library filesize is now 2.3MB and 400KB when zipped :heart_eyes: And more savings / lazyloading as the application starts taking shape (ie. splitting up lib.babylon.js into different chunks).

lib.preload.js

/* ====================================== */
/*                                        */
/*         	  BABYLON (PRELOAD)           */
/*                                        */
/* ====================================== */

export { Axis } from '@babylonjs/core/Maths/math.axis.js'
export { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js'
export { Curve3, BezierCurve, Angle, Arc2, Path2, Path3D } from '@babylonjs/core/Maths/math.path.js'
export { 
	Vector2, 
	Vector3, 
	Vector4,
	Quaternion,
	Matrix } from '@babylonjs/core/Maths/math.vector.js'

export * as EngineConstants from '@babylonjs/core/Engines/constants.js'

export const MeshConstants = {
	FRONTSIDE: 0,
	BACKSIDE: 1,
	DOUBLESIDE: 2,
	DEFAULTSIDE: 0,
	NO_CAP: 0,
	CAP_START: 1,
	CAP_END: 2,
	CAP_ALL: 3,
	NO_FLIP: 0,
	FLIP_TILE: 1,
	ROTATE_TILE: 2,
	FLIP_ROW: 3,
	ROTATE_ROW: 4,
	FLIP_N_ROTATE_TILE: 5,
	FLIP_N_ROTATE_ROW: 6,
	CENTER: 0,
	LEFT: 1,
	RIGHT: 2,
	TOP: 3,
	BOTTOM: 4,
	INSTANCEDMESH_SORT_TRANSPARENT: false
}

export const CameraConstants = {
	PERSPECTIVE_CAMERA: 0,
	ORTHOGRAPHIC_CAMERA: 1,
	FOVMODE_VERTICAL_FIXED: 0,
	FOVMODE_HORIZONTAL_FIXED: 1,
	RIG_MODE_NONE: 0,
	RIG_MODE_STEREOSCOPIC_ANAGLYPH: 10,
	RIG_MODE_STEREOSCOPIC_SIDEBYSIDE_PARALLEL: 11,
	RIG_MODE_STEREOSCOPIC_SIDEBYSIDE_CROSSEYED: 12,
	RIG_MODE_STEREOSCOPIC_OVERUNDER: 13,
	RIG_MODE_STEREOSCOPIC_INTERLACED: 14,
	RIG_MODE_VR: 20,
	RIG_MODE_CUSTOM: 22
}

lib.babylon.js

/* ====================================== */
/*                                        */
/*         	 BABYLON (LAZY-LOAD)          */
/*                                        */
/* ====================================== */

export * from './lib.preload.js'

// ------ ENGINE ------

export { Engine } from '@babylonjs/core/Engines/engine.js'
export { WebGPUEngine } from '@babylonjs/core/Engines/webgpuEngine.js'
// export { ThinEngine } from '@babylonjs/core/Engines/thinEngine.js'
export { NullEngine } from '@babylonjs/core/Engines/nullEngine.js'

/* https://forum.babylonjs.com/t/webgpu-babylonjs-unable-to-compile-effect/27771/3 */
export * as EngineExtensions from '@babylonjs/core/Engines/WebGPU/Extensions' /* DEPENDENCIES */

// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.dynamicTexture.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.dynamicBuffer.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.uniformBuffer.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.computeShader.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.storageBuffer.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.multiRender.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.readTexture.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.renderTargetCube.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.multiview.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.renderTargetCube.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.externalTexture.js'
// import '@babylonjs/core/Engines/WebGPU/Extensions/engine.videoTexture.js'

// ------ SCENE / RENDERERS ------

export { Scene } from '@babylonjs/core/scene.js'
export { BoundingBoxRenderer } from '@babylonjs/core/Rendering/boundingBoxRenderer.js'
export * as SceneHelpers from '@babylonjs/core/Helpers/sceneHelpers.js' // BUGFIX

// ------ LAYERS ------

export { GlowLayer } from '@babylonjs/core/Layers/glowLayer.js'
export { HighlightLayer } from '@babylonjs/core/Layers/highlightLayer.js'
export { EffectLayer } from '@babylonjs/core/Layers/effectLayer.js'
export { Layer } from '@babylonjs/core/Layers/layer.js'

// ------ LIGHTS ------

export { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js'
export { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight.js'
export { PointLight } from '@babylonjs/core/Lights/pointLight.js'
export { ShadowLight } from '@babylonjs/core/Lights/shadowLight.js'
export { SpotLight } from '@babylonjs/core/Lights/spotLight.js'

// ------ CAMERAS ------

export { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera.js'
export { TargetCamera } from '@babylonjs/core/Cameras/targetCamera.js'
export { FreeCamera } from '@babylonjs/core/Cameras/freeCamera.js'
export { UniversalCamera } from '@babylonjs/core/Cameras/universalCamera.js'

/* 
	export { TouchCamera } from '@babylonjs/core/Cameras/touchCamera.js'
	export { DeviceOrientationCamera } from '@babylonjs/core/Cameras/deviceOrientationCamera.js'
	export { FlyCamera } from '@babylonjs/core/Cameras/flyCamera.js'
	export { FollowCamera, ArcFollowCamera } from '@babylonjs/core/Cameras/followCamera.js'
	export { GamepadCamera } from '@babylonjs/core/Cameras/gamepadCamera.js'
	export { 
		StereoscopicFreeCamera,
		StereoscopicUniversalCamera,
		StereoscopicGamepadCamera,
		StereoscopicArcRotateCamera,
		StereoscopicScreenUniversalCamera
		} from '@babylonjs/core/Cameras/Stereoscopic/index.js'
	export { VirtualJoysticksCamera } from '@babylonjs/core/Cameras/virtualJoysticksCamera.js'
	export * as RigModes from '@babylonjs/core/Cameras/RigModes/index.js'
	export * as Inputs from '@babylonjs/core/Cameras/Inputs/index.js'
	export { CameraInputsManager } from '@babylonjs/core/Cameras/cameraInputsManager.js'
	export { FreeCameraInputsManager } from '@babylonjs/core/Cameras/freeCameraInputsManager.js'
	export { ArcRotateCameraInputsManager } from '@babylonjs/core/Cameras/arcRotateCameraInputsManager.js'
	export { FlyCameraInputsManager } from '@babylonjs/core/Cameras/flyCameraInputsManager.js'
	export { FollowCameraInputsManager } from '@babylonjs/core/Cameras/followCameraInputsManager.js'
	export * as VR from '@babylonjs/core/Cameras/VR/index.js'
*/

// ------ MATERIALS ------

export { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
export { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial.js'

export { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial.js'
export { PBRMetallicRoughnessMaterial } from '@babylonjs/core/Materials/PBR/pbrMetallicRoughnessMaterial.js'

// ------ TEXTURES ------

import '@babylonjs/core/Materials/Textures/Loaders' /* DEPENDENCIES */

export { Texture } from '@babylonjs/core/Materials/Textures/texture.js'
export { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js'
export { ExternalTexture } from '@babylonjs/core/Materials/Textures/externalTexture.js'
export { HDRCubeTexture } from '@babylonjs/core/Materials/Textures/hdrCubeTexture.js'
export { HtmlElementTexture } from '@babylonjs/core/Materials/Textures/htmlElementTexture.js'
export { MirrorTexture } from '@babylonjs/core/Materials/Textures/mirrorTexture.js'
export { VideoTexture } from '@babylonjs/core/Materials/Textures/videoTexture.js'
export { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture.js'

// ------ ADVANCED MATERIALS ------

export { FurMaterial } from '@babylonjs/materials/fur/index.js'
export { GridMaterial } from '@babylonjs/materials/grid/index.js'

// ------ MESHES / BUILDERS ------

export { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
// export { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder.js'  

export { CreateBoxVertexData, CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder.js'
export { CreateCapsuleVertexData, CreateCapsule } from '@babylonjs/core/Meshes/Builders/capsuleBuilder.js'
export { CreateCylinderVertexData, CreateCylinder } from '@babylonjs/core/Meshes/Builders/cylinderBuilder.js'
export { CreateDiscVertexData, CreateDisc } from '@babylonjs/core/Meshes/Builders/discBuilder.js'
export { CreateGoldbergVertexData, CreateGoldberg } from '@babylonjs/core/Meshes/Builders/goldbergBuilder.js'
export { CreateGroundVertexData, CreateGround } from '@babylonjs/core/Meshes/Builders/groundBuilder.js'
export { CreateIcoSphereVertexData, CreateIcoSphere } from '@babylonjs/core/Meshes/Builders/icoSphereBuilder.js'
export { CreatePlaneVertexData, CreatePlane } from '@babylonjs/core/Meshes/Builders/planeBuilder.js'
export { CreatePolygonVertexData, CreatePolygon } from '@babylonjs/core/Meshes/Builders/polygonBuilder.js'
export { CreatePolyhedronVertexData, CreatePolyhedron } from '@babylonjs/core/Meshes/Builders/polyhedronBuilder.js'
export { CreateTiledBoxVertexData, CreateTiledBox } from '@babylonjs/core/Meshes/Builders/tiledBoxBuilder.js'
export { CreateTorusVertexData, CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder.js'
export { CreateTorusKnotVertexData, CreateTorusKnot } from '@babylonjs/core/Meshes/Builders/torusKnotBuilder.js'

export { 
	CreateGreasedLineMaterial, 
	CreateGreasedLine, 
	CompleteGreasedLineWidthTable,
	CompleteGreasedLineColorTable } from '@babylonjs/core/Meshes/Builders/greasedLineBuilder.js' // non-standard (ribbon)
export { CreateLathe } from '@babylonjs/core/Meshes/Builders/latheBuilder.js' // non-standard (ribbon)
export { CreateTube } from '@babylonjs/core/Meshes/Builders/tubeBuilder.js' // non-standard (ribbon)
export { CreateGeodesic } from '@babylonjs/core/Meshes/Builders/geodesicBuilder.js' // non-standard (polyhedron)
export { CreateHemisphere } from '@babylonjs/core/Meshes/Builders/hemisphereBuilder.js' // non-standard (sphere / disc)
export { 
	CreateLineSystemVertexData, 
	CreateDashedLinesVertexData, 
	CreateLineSystem, 
	CreateDashedLines, 
	CreateLines } from '@babylonjs/core/Meshes/Builders/linesBuilder.js' // non-standard 
export { ExtrudeShape, ExtrudeShapeCustom } from '@babylonjs/core/Meshes/Builders/shapeBuilder.js' // non-standard
export { CreateSphereVertexData, CreateSphere } from '@babylonjs/core/Meshes/Builders/sphereBuilder.js'
export { 
	CreateTextShapePaths, 
	CreateText } from '@babylonjs/core/Meshes/Builders/textBuilder.js' // non-standard
export { CreateDecal } from '@babylonjs/core/Meshes/Builders/decalBuilder.js' // non-standard

index.js


export * from './lib.preload.js'

export async function LoadBabylon() {
	const timestamp = new Date()
	return new Promise( async (resolve,reject) => {

		if (globalThis.BB) return resolve( globalThis.BB )

		const BB = await import('./lib.babylon.js')
		globalThis.BB = BB

		const elapsed = new Date() - timestamp 
		SAY(`✅ BABYLON LAZYLOAD: ${elapsed}ms`)
		return resolve( globalThis.BB )
	})
}
1 Like

I also made a little import viewer so I could check each module before compile (using function string size):

Maybe this could be used to make a sort of custom build interface - parsing each export and filepath?

Also here is the WebGPU part of the application:


/* ====================================== */
/*                                        */
/*         	   BABYLON (ENGINE)           */
/*                                        */
/* ====================================== */

export let staticWebGPU = null

export async function WebGPUConfig( debug ) {
	if (staticWebGPU) return staticWebGPU 

	staticWebGPU = {
		glslangOptions: {
			jsPath: import.meta.env.VITE_BASE + '/babylon/glslang.js',
			wasmPath: import.meta.env.VITE_BASE + '/babylon/glslang.wasm'
		},
		twgslOptions: {
			jsPath: import.meta.env.VITE_BASE + '/babylon/twgsl.js',
			wasmPath: import.meta.env.VITE_BASE + '/babylon/twgsl.wasm'
		}
	}

	SAY(`👔 WASM`)
	return staticWebGPU
}


export async function CreateEngine( canvas, debug ) {
	
	return new Promise( async (resolve,reject) => {

		try { 
			if (!canvas) throw `❌ no canvas`
			if ('gpu' in navigator) {
				SAY(`🌁 FETCH GPU / WASM`)

				const webgpuConfig = await WebGPUConfig( debug )
				const engine = new BB.WebGPUEngine( canvas )

				SAY(`🌁 INIT GPU / WASM`)

				await engine.initAsync(
					{ ...webgpuConfig.glslangOptions },
					{ ...webgpuConfig.twgslOptions }).catch(err => reject(err))

				SAY(`✅ loaded GLSLANG / TWGSL and WebGPU`)
				resolve( engine )
			} else {
				if (debug) say(`🏞️ falling back to WebGL`)
				const engine = new BB.Engine( canvas, true)
				resolve(engine)
			}
		} catch(err) {
			reject(err)
		}
	})
}

1 Like