Tree Shaking Three.js

Hello!

I’m building a web app with Three.js. Our main focus is instance loading on the first visit of the user, therefore I explore possibilities to reduce the file size of my bundle. When I look what makes the biggest portion of the bundle I see the whole Three.js library. Therefore I tried to tree shake parts which I don’t need.

I created a repo which demonstrates my approach (you can find it here: https://github.com/tschoartschi/tree-shake-threejs). But I’m not sure if my solution is the best or if there are ways to get even better results. I was inspired a lot by the following issue: https://github.com/mrdoob/three.js/issues/9403

Looking forward to an interesting discussion

1 Like

You need a bad connection to not load THREE alone instantly already, or what is your actual usecase? Like a background or widget on a frontpage?

1 Like

Do you mean just three.min.js or all the stuff from examples?

This seems like a reasonable goal. Some suggestions were posted on Stack Overflow here that may be helpful: https://stackoverflow.com/questions/41538767/how-do-i-tree-shake-three-js-using-webpack-or-rollup

4 Likes

@Fyrestar perhaps your idea of a “bad connection” is very different from, say, someone who lives far out in the countryside and is lucky to get 1mbs on a mobile connection.

Also, there is parsing time of JS to consider.

Personally, I take less than 5 seconds to interactive on an old Android (Moto G3) on a slow 3G connection as my benchmark. In this case, the amount of JS being parsed is very important, and the difference between a 500kb file and a 1mb file (before gzipping) might be as much as 5-10 seconds added to the first load time.

So, this is a valid question.

3 Likes

@tschoartschi the most I’ve been able to do within a reasonable amount of effort is reduce the amount three.js adds to my build from ~600kb to ~300kb (unminified).

To do this I had to stop using NPM ( I haven’t found any way to get tree shaking to work when importing from the NPM module ).

Instead, I downloaded the source to /vendor/three and created a file three.custom.js:

export { WebGLRenderer } from './vendor/three/src/renderers/WebGLRenderer.js';
export { Scene } from './vendor/three/src/scenes/Scene.js';
export { Mesh } from './vendor/three/src/objects/Mesh.js';
export * from './vendor/three/src/geometries/Geometries.js';
export * from './vendor/three/src/materials/Materials.js';
export { TextureLoader } from './vendor/three/src/loaders/TextureLoader.js';
export { LoadingManager } from './vendor/three/src/loaders/LoadingManager.js';
export { FontLoader } from './vendor/three/src/loaders/FontLoader.js';
export { Cache } from './vendor/three/src/loaders/Cache.js';
export { PerspectiveCamera } from './vendor/three/src/cameras/PerspectiveCamera.js';
export { BufferGeometry } from './vendor/three/src/core/BufferGeometry.js';
export * from './vendor/three/src/core/BufferAttribute.js';
export { _Math } from './vendor/three/src/math/Math.js';
export { Vector2 } from './vendor/three/src/math/Vector2.js';
export { Vector3 } from './vendor/three/src/math/Vector3.js';
export { Color } from './vendor/three/src/math/Color.js';
export * from './vendor/three/src/constants.js';

Your requirements will probably differ from this… but then, import the elements you need in your app. For example, in the main file you will probably need at least:

import { WebGLRenderer, Scene, PerspectiveCamera } from './three.custom.js`;

At this point, actually the file size is still not reduced. Too many dependencies with the three.js src for tree shaking to work. So I had to go through and remove many import statements within the src, in particular from src/geometries/Geometries.js, src/materials/Materials.js and so on.

It took me a couple of hours, and I stopped when I reached a size I was happy with (I could probably reduce it much more), but along with removing some other dependencies ( I was loading lots of unneeded polyfills with babel-polyfill for example ), my overall minified file size went from ~700kb to ~300kb.

Testing the page on www.webpagetest.org set to a slow 3g network and Moto G4, my first page load time went from ~17s to ~6s.

4 Likes

Yes, as i said, the only reason i see is for really bad connections, but it also depends on the server and everything else being loaded / processed, since asynchronous loading resources will throttle it too. V8 caches compiled code. Parsing 5-10 seconds? Well i mean, if you want to support very old hardware in the outback of nowhere, this sure makes sense, the target group might be not that big (and young) though :yum:

I mean you probably optimized everything else already, you probably already serve it statically and inlined in just 1 request. But i’m curious what the app actually is about. I’ve never seen people complaining about an app loading 0.5 - 3 seconds, it’s a whole different thing for a website… a frontpage being a heavyweight cause of unnecessary header background thing that isn’t even interactive… there it gets annoying, but even with a slow connection it feels “ok”, since the images are probably even larger than THREE.

If i want to reduce THREE i compile it with just those includes i need, like for using in workers. I can ensure everything will work, if you care about every single byte for reasons, it’s a different story… biggest ones are probably the optional renderers.

1 Like

That’s what i mean, yes that makes sense then. If it’s about a single animated character one could also probably try a super minimalistic lightweight WebGL lib.

If it’s just about a widget it isn’t the main thing people wait for before they can do anything. But my Galaxy S4 actually loads almost just as desktop, i didn’t benchmark things there, since i never noticed such a big gap except for a bad connection.

Edit: i mean if the widget is like a animated assistant that isn’t necessarily relevant for the shop process, if it’s like viewing a product then it’s super relevant of course :smile:

1 Like

It’s kind of off topic because it’s not about tree shaking, but the widget is an interactive product configurator. So it’s even more than a viewer :smiley:

But back to topic. If someone knows tipps and tricks about tree shaking, let me know :wink:

I just posted this as I too am having difficulty getting the size down. I have created a custom main app file and rollup build and only include what I need to bundle.

This helps strips the shader code which adds about 150kb.

1 Like

I’m facing a problem of my own. My app needs to stay seperate to three.js so reference it globally with THREE.module. Because I am scrambling part of the code and prepending three.js.

I have figured out this is very elegant. And if you reference three as global in the rollup build it will add everything as THREE and not include it into your app bundle. This is for a seperate build of said 3rd party project that relies on three.

import {
//MeshBasicMaterial,
RawShaderMaterial,
BoxBufferGeometry,
Mesh,
Group,
LinearMipMapLinearFilter,
LinearFilter,
DoubleSide
} from ‘three’;

However doing so like this if I tried to bundle that external library back into the three.js build to make a self contained package. It’s trying to duplicate the included code and even include a whole minified version.

I have included similar three modules but I have to hard code reference the files like

import { EventDispatcher } from ‘…/…/three.js/src/core/EventDispatcher’;

This is the only way to get it bundling properly. Using the nasty node import reference its not referencing the source code but including duplicated code ?

How do I make this all work properly ?.

The third party modules shouldn’t be referencing hard coded paths like that but need to reference the sources still. not attempt to find the module installed via npm I guess ?

Here is a few examples to explain my problems.

This module I have made I have to hardcode the import paths like

So I can include it into the three bundle like

export { OmniToneAudio } from ‘…/three-vr-omnitone/src/OmniToneAudio.js’;

If I can renference the import like

import { AudioContext } from 'three';

And reference the source code instead of duplicating bundled code. that could be useful and help me out with another library I have reconstructed and fixed up to es6 like this where I am importing the suggested way. But

it seems this suggested way is designed to bundle modules in from npm instead ! That is not what I want.

I am trying to bundle them into the main three bundle.

If I import like so there is duplicated code, it increases the file size. If I set three as a global variable to THREE it messes up the three bundling.

Bit of a mess. and have to use hardcoded paths which means the third party module can’t be bundled seperate and treat three as global.

import {
//MeshBasicMaterial,
RawShaderMaterial,
BoxBufferGeometry,
Mesh,
Group,
LinearMipMapLinearFilter,
LinearFilter,
DoubleSide
} from '../../three.js/src/Three.js';

Just to show how single minded and bad that import solution is above as I’m working with three sources not nasty npm imports.

I have to include the sources like this therefore the external project becomes tied into a three.js bundle. This won’t include duplicated code. It will therefore be referenced like the other three.js modules

import { RawShaderMaterial } from '../../three.js/src/materials/RawShaderMaterial';
import { BoxBufferGeometry } from '../../three.js/src/geometries/BoxGeometry';
import { Mesh } from '../../three.js/src/objects/Mesh';
import { Group } from '../../three.js/src/objects/Group';

import { LinearMipMapLinearFilter,LinearFilter, DoubleSide } from ‘…/…/three.js/src/constants’;

And I have exporting like

export { TextBitmap, SingleTextBitmap } from '../three-bmfont-text/src/three-bmfont-text.js';

Ok I have reported back there in regards to stripping out shaders. But not sure if that will help me figure out the tree shaking problem I am experiencing.

It would be nice to use the elegant import system but it maps to the sources when making a three bundle. Instead its looking for multiple versions of npm three modules and bundling them in.

You can make it reference three for the import in a 3rd party module. And set the rollup config to three as global. That is fine it wont bundle three that way then.

But doing it the other way importing that module into a custom three bundle does not work.

I confirm that tree-shaking works and works correctly. You just need to link to three.module.js version

According to latest tweets about upcoming webpack v4 release, there might be some positive changes soon.

As for right now, following what Sean Larkin did with d3 library on his tweet, in our case simply adding "sideEffects": false to three/package.json just gives no effect. Seems like supporting this will require additional changes in three codebase. I am not much faimiliar with such things, just spreading some news.

Additional info that I’ve found:


@nulltails thanks for the info :slight_smile: would be interested to see if rollup could also handle Three.js better in the future. For now we rely on rollup and I don’t want to add a new build tool if it’s not totally necessary

Can you provide more details what size reduction you have been able to achieve?

2 Likes

@sasha240100 can you share your technique? I haven’t been able to get any useful size reduction using automatic tree shaking with any of the methods described.

1 Like