Skip to content

Conversation

CodyJasonBennett
Copy link
Contributor

@CodyJasonBennett CodyJasonBennett commented Oct 7, 2023

Related issue: #24595

Description

Backports tree-shaking fixes for addons from three-stdlib. This enables three/addons in #26910.

I haven't touched WebGPU or nodes code since they are WIP and won't be included in #26910, but I have a branch handy where I've already investigated tree-shaking prior which is uniquely effectful (related: #25526).

Progress:
  • three/addons/animation
    • three/addons/animation/AnimationClipCreator.js
    • three/addons/animation/CCDIKSolver.js
    • three/addons/animation/MMDAnimationHelper.js
    • three/addons/animation/MMDPhysics.js
  • three/addons/cameras
    • three/addons/cameras/CinematicCamera.js
  • three/addons/capabilities
    • three/addons/capabilities/WebGL.js
  • three/addons/controls
    • three/addons/controls/ArcballControls.js
    • three/addons/controls/DragControls.js
    • three/addons/controls/FirstPersonControls.js
    • three/addons/controls/FlyControls.js
    • three/addons/controls/MapControls.js
    • three/addons/controls/OrbitControls.js
    • three/addons/controls/PointerLockControls.js
    • three/addons/controls/TrackballControls.js
    • three/addons/controls/TransformControls.js
  • three/addons/csm
    • three/addons/csm/CSM.js
    • three/addons/csm/CSMFrustum.js
    • three/addons/csm/CSMHelper.js
    • three/addons/csm/CSMShader.js
  • three/addons/curves
    • three/addons/curves/CurveExtras.js
    • three/addons/curves/NURBSCurve.js
    • three/addons/curves/NURBSSurface.js
    • three/addons/curves/NURBSUtils.js
  • three/addons/effects
    • three/addons/effects/AnaglyphEffect.js
    • three/addons/effects/AsciiEffect.js
    • three/addons/effects/OutlineEffect.js
    • three/addons/effects/ParallaxBarrierEffect.js
    • three/addons/effects/PeppersGhostEffect.js
    • three/addons/effects/StereoEffect.js
  • three/addons/environments
    • three/addons/environments/DebugEnvironment.js
    • three/addons/environments/RoomEnvironment.js
  • three/addons/exporters
    • three/addons/exporters/DRACOExporter.js
    • three/addons/exporters/EXRExporter.js
    • three/addons/exporters/GLTFExporter.js
    • three/addons/exporters/KTX2Exporter.js
    • three/addons/exporters/MMDExporter.js
    • three/addons/exporters/OBJExporter.js
    • three/addons/exporters/PLYExporter.js
    • three/addons/exporters/STLExporter.js
    • three/addons/exporters/USDZExporter.js
  • three/addons/geometries
    • three/addons/geometries/BoxLineGeometry.js
    • three/addons/geometries/ConvexGeometry.js
    • three/addons/geometries/DecalGeometry.js
    • three/addons/geometries/ParametricGeometries.js
    • three/addons/geometries/ParametricGeometry.js
    • three/addons/geometries/RoundedBoxGeometry.js
    • three/addons/geometries/TeapotGeometry.js
    • three/addons/geometries/TextGeometry.js
  • three/addons/helpers
    • three/addons/helpers/LightProbeHelper.js
    • three/addons/helpers/OctreeHelper.js
    • three/addons/helpers/PositionalAudioHelper.js
    • three/addons/helpers/RectAreaLightHelper.js
    • three/addons/helpers/VertexNormalsHelper.js
    • three/addons/helpers/VertexTangentsHelper.js
    • three/addons/helpers/ViewHelper.js
  • three/addons/interactive
    • three/addons/interactive/HTMLMesh.js
    • three/addons/interactive/InteractiveGroup.js
    • three/addons/interactive/SelectionBox.js
    • three/addons/interactive/SelectionHelper.js
  • three/addons/lights
    • three/addons/lights/IESSpotLight.js
    • three/addons/lights/LightProbeGenerator.js
    • three/addons/lights/RectAreaLightUniformsLib.js
  • three/addons/lines
    • three/addons/lines/Line2.js
    • three/addons/lines/LineGeometry.js
    • three/addons/lines/LineMaterial.js
    • three/addons/lines/LineSegments2.js
    • three/addons/lines/LineSegmentsGeometry.js
    • three/addons/lines/Wireframe.js
    • three/addons/lines/WireframeGeometry2.js
  • three/addons/loaders
    • three/addons/loaders/3DMLoader.js
    • three/addons/loaders/3MFLoader.js
    • three/addons/loaders/AMFLoader.js
    • three/addons/loaders/BVHLoader.js
    • three/addons/loaders/ColladaLoader.js
    • three/addons/loaders/DDSLoader.js
    • three/addons/loaders/DRACOLoader.js
    • three/addons/loaders/EXRLoader.js
    • three/addons/loaders/FBXLoader.js
    • three/addons/loaders/FontLoader.js
    • three/addons/loaders/GCodeLoader.js
    • three/addons/loaders/GLTFLoader.js
    • three/addons/loaders/HDRCubeTextureLoader.js
    • three/addons/loaders/IESLoader.js
    • three/addons/loaders/KMZLoader.js
    • three/addons/loaders/KTX2Loader.js
    • three/addons/loaders/KTXLoader.js
    • three/addons/loaders/LDrawLoader.js
    • three/addons/loaders/LUT3dlLoader.js
    • three/addons/loaders/LUTCubeLoader.js
    • three/addons/loaders/LWOLoader.js
    • three/addons/loaders/LogLuvLoader.js
    • three/addons/loaders/LottieLoader.js
    • three/addons/loaders/MD2Loader.js
    • three/addons/loaders/MDDLoader.js
    • three/addons/loaders/MMDLoader.js
    • three/addons/loaders/MTLLoader.js
    • three/addons/loaders/NRRDLoader.js
    • three/addons/loaders/OBJLoader.js
    • three/addons/loaders/PCDLoader.js
    • three/addons/loaders/PDBLoader.js
    • three/addons/loaders/PLYLoader.js
    • three/addons/loaders/PVRLoader.js
    • three/addons/loaders/RGBELoader.js
    • three/addons/loaders/RGBMLoader.js
    • three/addons/loaders/STLLoader.js
    • three/addons/loaders/SVGLoader.js
    • three/addons/loaders/TDSLoader.js
    • three/addons/loaders/TGALoader.js
    • three/addons/loaders/TIFFLoader.js
    • three/addons/loaders/TTFLoader.js
    • three/addons/loaders/TiltLoader.js
    • three/addons/loaders/USDZLoader.js
    • three/addons/loaders/VOXLoader.js
    • three/addons/loaders/VRMLLoader.js
    • three/addons/loaders/VTKLoader.js
    • three/addons/loaders/XYZLoader.js
    • three/addons/loaders/lwo/IFFParser.js
    • three/addons/loaders/lwo/LWO2Parser.js
    • three/addons/loaders/lwo/LWO3Parser.js
  • three/addons/materials
    • three/addons/materials/MeshGouraudMaterial.js
  • three/addons/math
    • three/addons/math/Capsule.js
    • three/addons/math/ColorConverter.js
    • three/addons/math/ConvexHull.js
    • three/addons/math/ImprovedNoise.js
    • three/addons/math/Lut.js
    • three/addons/math/MeshSurfaceSampler.js
    • three/addons/math/OBB.js
    • three/addons/math/Octree.js
    • three/addons/math/SimplexNoise.js
  • three/addons/misc
    • three/addons/misc/ConvexObjectBreaker.js
    • three/addons/misc/GPUComputationRenderer.js
    • three/addons/misc/Gyroscope.js
    • three/addons/misc/MD2Character.js
    • three/addons/misc/MD2CharacterComplex.js
    • three/addons/misc/MorphAnimMesh.js
    • three/addons/misc/MorphBlendMesh.js
    • three/addons/misc/ProgressiveLightMap.js
    • three/addons/misc/RollerCoaster.js
    • three/addons/misc/TubePainter.js
    • three/addons/misc/Volume.js
    • three/addons/misc/VolumeSlice.js
  • three/addons/modifiers
    • three/addons/modifiers/CurveModifier.js
    • three/addons/modifiers/EdgeSplitModifier.js
    • three/addons/modifiers/SimplifyModifier.js
    • three/addons/modifiers/TessellateModifier.js
  • three/addons/objects
    • three/addons/objects/GroundProjectedSkybox.js
    • three/addons/objects/Lensflare.js
    • three/addons/objects/MarchingCubes.js
    • three/addons/objects/Reflector.js
    • three/addons/objects/ReflectorForSSRPass.js
    • three/addons/objects/Refractor.js
    • three/addons/objects/ShadowMesh.js
    • three/addons/objects/Sky.js
    • three/addons/objects/Water.js
    • three/addons/objects/Water2.js
  • three/addons/physics
    • three/addons/physics/AmmoPhysics.js
    • three/addons/physics/RapierPhysics.js
  • three/addons/postprocessing
    • three/addons/postprocessing/AfterimagePass.js
    • three/addons/postprocessing/BloomPass.js
    • three/addons/postprocessing/BokehPass.js
    • three/addons/postprocessing/ClearPass.js
    • three/addons/postprocessing/CubeTexturePass.js
    • three/addons/postprocessing/DotScreenPass.js
    • three/addons/postprocessing/EffectComposer.js
    • three/addons/postprocessing/FilmPass.js
    • three/addons/postprocessing/GlitchPass.js
    • three/addons/postprocessing/HalftonePass.js
    • three/addons/postprocessing/LUTPass.js
    • three/addons/postprocessing/MaskPass.js
    • three/addons/postprocessing/OutlinePass.js
    • three/addons/postprocessing/OutputPass.js
    • three/addons/postprocessing/Pass.js
    • three/addons/postprocessing/RenderPass.js
    • three/addons/postprocessing/RenderPixelatedPass.js
    • three/addons/postprocessing/SAOPass.js
    • three/addons/postprocessing/SMAAPass.js
    • three/addons/postprocessing/SSAARenderPass.js
    • three/addons/postprocessing/SSAOPass.js
    • three/addons/postprocessing/SSRPass.js
    • three/addons/postprocessing/SavePass.js
    • three/addons/postprocessing/ShaderPass.js
    • three/addons/postprocessing/TAARenderPass.js
    • three/addons/postprocessing/TexturePass.js
    • three/addons/postprocessing/UnrealBloomPass.js
  • three/addons/renderers
    • three/addons/renderers/CSS2DRenderer.js
    • three/addons/renderers/CSS3DRenderer.js
    • three/addons/renderers/Projector.js
    • three/addons/renderers/SVGRenderer.js
  • three/addons/shaders
    • three/addons/shaders/ACESFilmicToneMappingShader.js
    • three/addons/shaders/AfterimageShader.js
    • three/addons/shaders/BasicShader.js
    • three/addons/shaders/BleachBypassShader.js
    • three/addons/shaders/BlendShader.js
    • three/addons/shaders/BokehShader.js
    • three/addons/shaders/BokehShader2.js
    • three/addons/shaders/BrightnessContrastShader.js
    • three/addons/shaders/ColorCorrectionShader.js
    • three/addons/shaders/ColorifyShader.js
    • three/addons/shaders/ConvolutionShader.js
    • three/addons/shaders/CopyShader.js
    • three/addons/shaders/DOFMipMapShader.js
    • three/addons/shaders/DepthLimitedBlurShader.js
    • three/addons/shaders/DigitalGlitch.js
    • three/addons/shaders/DotScreenShader.js
    • three/addons/shaders/ExposureShader.js
    • three/addons/shaders/FXAAShader.js
    • three/addons/shaders/FilmShader.js
    • three/addons/shaders/FocusShader.js
    • three/addons/shaders/FreiChenShader.js
    • three/addons/shaders/GammaCorrectionShader.js
    • three/addons/shaders/GodRaysShader.js
    • three/addons/shaders/HalftoneShader.js
    • three/addons/shaders/HorizontalBlurShader.js
    • three/addons/shaders/HorizontalTiltShiftShader.js
    • three/addons/shaders/HueSaturationShader.js
    • three/addons/shaders/KaleidoShader.js
    • three/addons/shaders/LuminosityHighPassShader.js
    • three/addons/shaders/LuminosityShader.js
    • three/addons/shaders/MMDToonShader.js
    • three/addons/shaders/MirrorShader.js
    • three/addons/shaders/NormalMapShader.js
    • three/addons/shaders/OutputShader.js
    • three/addons/shaders/RGBShiftShader.js
    • three/addons/shaders/SAOShader.js
    • three/addons/shaders/SMAAShader.js
    • three/addons/shaders/SSAOShader.js
    • three/addons/shaders/SSRShader.js
    • three/addons/shaders/SepiaShader.js
    • three/addons/shaders/SobelOperatorShader.js
    • three/addons/shaders/SubsurfaceScatteringShader.js
    • three/addons/shaders/TechnicolorShader.js
    • three/addons/shaders/ToonShader.js
    • three/addons/shaders/TriangleBlurShader.js
    • three/addons/shaders/UnpackDepthRGBAShader.js
    • three/addons/shaders/VelocityShader.js
    • three/addons/shaders/VerticalBlurShader.js
    • three/addons/shaders/VerticalTiltShiftShader.js
    • three/addons/shaders/VignetteShader.js
    • three/addons/shaders/VolumeShader.js
    • three/addons/shaders/WaterRefractionShader.js
  • three/addons/textures
    • three/addons/textures/FlakesTexture.js
  • three/addons/utils
    • three/addons/utils/BufferGeometryUtils.js
    • three/addons/utils/CameraUtils.js
    • three/addons/utils/GPUStatsPanel.js
    • three/addons/utils/GeometryCompressionUtils.js
    • three/addons/utils/GeometryUtils.js
    • three/addons/utils/LDrawUtils.js
    • three/addons/utils/PackedPhongMaterial.js
    • three/addons/utils/SceneUtils.js
    • three/addons/utils/ShadowMapViewer.js
    • three/addons/utils/SkeletonUtils.js
    • three/addons/utils/TextureUtils.js
    • three/addons/utils/UVsDebug.js
    • three/addons/utils/WorkerPool.js
  • three/addons/webxr
    • three/addons/webxr/ARButton.js
    • three/addons/webxr/OculusHandModel.js
    • three/addons/webxr/OculusHandPointerModel.js
    • three/addons/webxr/Text2D.js
    • three/addons/webxr/VRButton.js
    • three/addons/webxr/XRButton.js
    • three/addons/webxr/XRControllerModelFactory.js
    • three/addons/webxr/XREstimatedLight.js
    • three/addons/webxr/XRHandMeshModel.js
    • three/addons/webxr/XRHandModelFactory.js
    • three/addons/webxr/XRHandPrimitiveModel.js
    • three/addons/webxr/XRPlanes.js

Issues

The issues I've come across for tree-shaking are:

  • top-level call expressions or new expressions which aren't marked with /* @__PURE__ */ or /* #__PURE__ */
    • Some built-ins like Object.create and FileReader as used here are properly impure, most others aren't
  • static or prototype field mutation which must be wrapped in an IIFE (static fields are ES2022, methods ES6)
    • I've tried pure Object.assign at the module level, but without a value attached to them, they get discarded
  • object or namespace member access which is considered for impure getters, whereas destructuring avoids this

Testing

For testing, I've written https://github.com/CodyJasonBennett/treeshake-cli which tests against the popular bundlers -- rollup, webpack, and esbuild (vite uses esbuild in dev and rollup in prod). Issues such as mentioned above are reported.

image

With npx treeshake-cli@latest examples/jsm/Addons.js from #26910:

image

Linting

For linting, so you can get a gentle nudge in CI (as it's likely best effort in core/nodes), I've come across https://github.com/lukastaegert/eslint-plugin-tree-shaking which does not implement support for pure annotations yet. I plan to implement this and possibly use it with the above CLI tool, but will be invaluable here for non-intrusive regression testing.

For files with IIFEs used for static fields, I've collapsed whitespace changes for the sake of review (for why this matters, see #26935 (comment)). In IDE, there will be lint warnings, so they will have to be formatted once greenlighted. See e4fa50b for affected files.

@CodyJasonBennett CodyJasonBennett marked this pull request as draft October 7, 2023 02:50
@gkjohnson
Copy link
Collaborator

gkjohnson commented Oct 7, 2023

Just a comment - this may be the kind of thing that's best done in stages so it's easier for project members to review and approve. Ie a PR for just adding @__PURE__ annotations or breaking the PRs up by folders. The amount of files changed is already fairly large. I support these changes, though.

@Mugen87
Copy link
Collaborator

Mugen87 commented Oct 7, 2023

Making a PR that just adds the @__PURE__ annotations sounds like a good start. Most of the diff should be gone if we have these changes in which makes it easier to review the other code.

@Mugen87
Copy link
Collaborator

Mugen87 commented Oct 9, 2023

There seems to be three recurring patterns used in this PR:

  1. Using the /* @__PURE__ */ annotation when creating instances of THREE objects. This is a know pattern from the core.
  2. Using Object.assign() (with /* @__PURE__ */) to assign fields the classes.
  3. Wrap certain code in IIFEs like when using UniformsUtils.merge() in MeshGouraudMaterial.

I'm familiar with the first one since we are already using this approach in the core.

Do you mind explain 2 and 3? It's important that all collaborators (including myself^^) fully understand these patterns to ensure maintenance. Especially the IIFE thing is something that could easily be overlooked in new code that is going to be added to the library.

@CodyJasonBennett
Copy link
Contributor Author

The use of Object.assign implements static class fields that are otherwise written as Class.property = value which is not tree-shakable. I still need to test whether the expression is tree-shaken at the module level or down by value between various tools. I ran into a similar issue with Nodes, but that was more complicated since a nodes registry had to be populated despite no actual reference (thus everything is effectful).

// Assumes tree-shaking is at module level
class Foo {}
/* @__PURE__ */ Object.assign( Foo, { staticProperty: true } );
// Assumes tree-shaking is at module level by value
const Foo = /* @__PURE__ */ ( () => {

    class Foo {}
    Foo.staticProperty = true

    return Foo;

} )();

MeshGouraudMaterial and others which have an IIFE in a similar manner I added inner annotations since it wasn't clear why that expression wasn't tree-shaking or was too complicated to annotate by hand such as libs (I'd be happy to if not for the diff in this PR). I believe it falls into #26912 (comment) when accessing UniformsLib, but I can only hypothesize why.

@LeviPesin
Copy link
Contributor

I ran into a similar issue with Nodes, but that was more complicated since a nodes registry had to be populated despite no actual reference (thus everything is effectful).

Related (in some way) issue: #25526

@CodyJasonBennett
Copy link
Contributor Author

CodyJasonBennett commented Oct 9, 2023

Looks like the latter case in #26912 (comment) is true. This adds a lot of whitespace nonsense, but otherwise the files are unmodified. Still, this would be good to split up since the rest of this PR very cleanly adds annotations.

Edit: I've since removed the whitespace changes, but they will need to be formatted with a linter. Actual code changes should just be the scope for the IIFE.

const CstParser = chevrotain.CstParser;
const { CstParser } = chevrotain;
Copy link
Contributor Author

@CodyJasonBennett CodyJasonBennett Oct 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a better example of #26912 (comment). I refactored this from a static method back to a class in b9d5666, figuring that destructuring from chevrotain rather than member access would tree-shake and that seems to be the case. Why they're not equivalent for tools, I'm not sure. Perhaps it's accounting for possible getters? This case is regarded in https://esbuild.github.io/api/#tree-shaking-and-side-effects.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In any event, I like this solution more than the previous refactoring 😇 .

@CodyJasonBennett
Copy link
Contributor Author

CodyJasonBennett commented Oct 10, 2023

I've also recapped everything found in review in the PR description for future reference with some notes on future quality control via linting. For this PR and future reference, I've written a CLI tool to E2E test with bundlers and diagnostics which I describe further there.

Comment on lines +14846 to +14850
return lottie;

} );

export default lottie;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from my tests, this optimization and most of the optimizations inside examples/jsm/libs are not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further review I have to affirm that they are necessary and precisely for the issues I outlined in the PR description. Rather than individually resolve these like I did in #26935, I thought it better to wrap their scopes in an IIFE for the sake of review.

Copy link
Contributor

@marcofugaro marcofugaro Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how you're testing, but I've made a repro branch of what I mean:
https://github.com/marcofugaro/treeshake-test/tree/addons-test

As you can see LottieLoader and lottie_canvas.module.js are tree-shaken already.
I also tested fflate.module.js, opentype.module.js, mmdparser.module.js but I believe most of jsm/libs are already tree-shakeable.

Copy link
Contributor

@marcofugaro marcofugaro Oct 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a matter of fact even other addons are tree-shaken already, I think having the proxy Addons.js is enough to make all addons tree-shakeable.
I tested with this

// Addons.js
export * from './loaders/LottieLoader.js';
export * from './loaders/TTFLoader.js';
export * from './loaders/TiltLoader.js';
export * from './loaders/USDZLoader.js';
export * from './loaders/VOXLoader.js';
export * from './loaders/VRMLLoader.js';
export * from './loaders/VTKLoader.js';
export * from './loaders/XYZLoader.js';
export * from './loaders/lwo/IFFParser.js';
export * from './loaders/lwo/LWO2Parser.js';
export * from './loaders/lwo/LWO3Parser.js';
// index.js
import { LottieLoader } from 'three/addons'

console.log(LottieLoader)

And they all were already tree-shaken except LottieLoader (I am not testing this PR, but default three). But maybe I'm wrong, could you test this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can provide repro of this if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per my last message, I explained the aforementioned issues and testing strategy in the PR description. I'm not testing any optimizers like Uglify or Terser (Vite optionally uses the latter), nor am I treating three as external in the event that package.sideEffects is interactive. Please understand that I'm regarding the lowest common denominator between minification strategies for creating a baseline for testing and the sake of observability, so even though tools like ESBuild and Rollup have access to package information for smarter traversal, others only work at the module level and can't afford such optimizations.

Copy link
Contributor

@marcofugaro marcofugaro Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I did read the description. My point is that in the real world, those issues don't prevent the tree-shaking of #26910.

Unlike the main bundle, we're not importing from a single file here. Since the package.sideEffects flag is present, the bundlers are able to completely discard the unused files, even if they have some side-effects in it.

I did a test with the full Addons.js from #26910 and as you can see Esbuild, Rollup and Webpack can successfully tree-shake the Addons.js file, even without minification (only three.module.js and FXAAShader.js are present in the final bundle):
https://github.com/marcofugaro/treeshake-test/blob/addons-test-full/dist/index-esbuild.js

My point is, the changes in jsm/libs and the IIFE wraps make maintaning the repo more difficult, while not providing any real improvement to tree-shaking.

@CodyJasonBennett
Copy link
Contributor Author

Closing since we can rely on package.sideEffects for #26910 as per the above review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants