-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tree-shaking broken by Object.defineProperties( ... ) #24006
Comments
You can test tree-shaking directly inside the tree.js repo by running
You can comment things in |
Ideas in no particular order: (a): rewrite Three.Legacy.js to avoid Object.defineProperties Might mean we can no longer warn about access to old properties with getters, only warn about methods? Vector3.prototype.getFoo = function () {
console.warn( ... );
} (b): publish production/development builds E.g. provide if ( process.env.ENV === 'development' ) {
Object.defineProperties( Vector3.prototype, { ... } );
} (c): provide legacy warnings through a separate export import { Vector3 } from 'three'; // no legacy warnings
import 'three/legacy'; // see all the legacy warnings, but lose tree-shaking
console.log( new Vector3() ); |
I think that the lines like https://github.com/mrdoob/three.js/blob/dev/src/core/Object3D.js#L66-L93 are also not tree-shakable, but not related to the Three.Legacy... |
If Object.defineProperties is in the constructor, that seems OK as far as rollup is concerned, but I haven't done tests in other bundlers. |
@donmccurdy I have a repro of the
// minimal example
export { PerspectiveCamera } from './cameras/PerspectiveCamera.js';
export { Scene } from './scenes/Scene.js';
export { WebGLRenderer } from './renderers/WebGLRenderer.js';
// unused class
export { InstancedBufferGeometry } from './core/InstancedBufferGeometry.js';
As you can see the class Now comment this line
And run again You can see that the unused |
Same, I also confirmed @marcofugaro's point this morning, tree-shaking behaves differently when importing from For example removing this one line of code makes a difference of nearly 100 KB of files that get bundled. three.js/src/animation/AnimationMixer.js Line 766 in ce66f72
|
@donmccurdy @marcofugaro to summarize I think we're dealing with at-least three problems in this issue that would need to be addressed:
All this to say, in my experience the only true way to have a library fully tree-shakeable is the classes all need to be self-contained, without any side-effect artifacts referencing them, and only imported by other classes that use them. |
This was discussed in the past, not really feasible and easy to maintain according to the core maintainers. |
Ya was expecting that to be the case, that's the trade-off though, more difficult to maintain with messier classes. Perhaps there's some other way we could rewrite Not a fan of the additional endpoints and modules myself, would rather keep everything imported from |
Currently failing for me, tested with Node.js v12.22.12, v14.19.2, and v17.5.0 —
|
An option (d): switch to semver, release a version 1.0.0 with all the legacy code removed, and take care of breaking changes in a more standard way going forwards. Few if any other libraries support backwards compatibility in the way that three.js does. Besides problems like this I think it also leads to "ugrade-itis" where users expect they should bump the three.js version number every month and from personal experience I know that's a bad idea and I don't thing it's something we need to, or should encourage. I know has been suggested and rejected before, but given that by now it's highly likely most people use three.js via NPM, perhaps it's time to revisit this? There's a standard method for updating NPM packages and three.js is one of the very few libraries that doesn't follow it, since we are releasing a new "minor" version every month. Minor versions are not supposed to contain breaking changes, however any three.js release may contain breaking changes. In practice, most three.js releases don't contain breaking changes so the majority of new releases would still be minor or patch releases, we could still stick with the monthly release schedule. The differences would be a) we remove all legacy code and b) we need to consider on release day whether the release will be major (breaking changes), minor (non-breaking changes), or patch (just bug fixes). I realize this is somewhat tangential to the current issue although it would solve one of the three problems @pschroen highlighted. If there's interest in discussing it further I'll move it to a new issue. |
Looks like a node issue.. try maybe Node 18.0.0? It is working for me with that version. Remember to |
That is just incorrect. Every version of three.js contains breaking changes. |
I suggest to not use this issue for discussions about the release cycle and versioning of Regarding #24006 (comment), I think we can solve the first point by just removing all The mentioned code in
For the future, I indeed suggest to keep deprecated properties in the respective classes for a certain period of time (e.g. one year). After that time, they can be removed. |
@Mugen87 I agree, also on discussion for the versioning being a different issue. It is related to this issue though, as pointed out by @looeee and a possible option for removing It's a complicated subject though and personally I'm on the fence about semver for three.js, lots of pros/cons. 😅 cc @mrdoob 👇 |
Adding some more detail here from this morning's troubleshooting, and possibly a fourth problem. The dead end I hit was on So current tree-shaking techniques mark them non-pure whether they have the pure annotation or not. Here's just one example of code being bundled when not in use.
And adding the pure annotation still doesn't work because the code is actually being used, and that's how tree-shaking works, so it's working properly here. Input: const _RESERVED_CHARS_RE = /*@__PURE__*/ '\\[\\]\\.:\\/';
...
const _wordCharOrDot = /*@__PURE__*/ '[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']'; Output: const _RESERVED_CHARS_RE = '\\[\\]\\.:\\/';
'[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']'; Note that all these issues are AST parsing of code outside the classes. When tree-shaking is done on an entry point file like For reference here's the Tree Shaking documentation from Webpack, I used Rollup for my tests but they handle the pure annotations the same, and so far the results seem to be the same across bundlers, so this would appear to be more of a three.js problem with how the code is being output if we are to continue using the flat |
So I'm going to propose something here that may seem radical to some, but it's not really in 2022, and I believe it's "the path of least resistance" here. Let me know if you guys agree but I view three.js as serving two use cases, as a three.js "application", and as a three.js class "library". For example I've personally used three.js on non-WebGL projects just to import from the internal math classes, I know I'm not the only one who does this.
What I'm proposing here is that three.js finally moves to a real ES module library, that would mean removing all CommonJS and Rollup, no bundling, just the separate classes. Even for the examples, loading them from It would require two pretty major changes though:
That's it! I know simpler said than done though right? If you guys are up for it I'm happy to help with this work. 😅 And dare I say, maybe this would be a big enough change to justify a switch to semver as well! 👀 |
Throwing out the build feels like a last-resort option to me. Rollup is a popular and well-respected tool, we're squarely in the use case it's made for, and the maintainers clearly don't see bundles and tree-shaking as incompatible goals. Our builds do much more than just concatenate files, all of which would be lost by publishing raw source files. I'm really hoping we can get a statement from a Rollup or Webpack maintainer about what's going on and how we or they could fix it. If the conclusion is really "bundlers are bad for libraries", then that's a huge deal for the wider JS ecosystem. It's not that we couldn't get by without Rollup, but something feels wrong if it's just three.js coming to the conclusion that this is necessary. (I have no opinion on switching to semver, removing Three.legacy.js, or removing CommonJS builds at this time.) |
That's fair, and I understand. Just wanted to put that out there as a pure ES module approach myself and @gordonnl have been doing with our work and used by OGL. It's been possible ever since Node.js added ES module support, and I would argue that's how ES modules were intended to be used, but that's for a different thread. 😅 So if we're aligned on keeping the flat I do still feel there is merit in changing the entry point to He also suggested agadoo which we should checkout. 🙂 |
And speaking of agadoo, spent some time tonight testing it out (nice one @Rich-Harris 🙌), as you pointed out.
Failed to tree-shake build/three.module.js From the
I don't like being the guy stirring the pot over all this stuff, though "Export plain functions" would include things like namespaces, so things like Here's also a good reference on this topic by @bluepnume. 😉 https://bluepnume.medium.com/javascript-tree-shaking-like-a-pro-7bf96e139eb7 |
Yeah, it seems much more likely that it's something to do with the way three.js is written so we should thoroughly investigate that first (albeit I kinda like @pschroen's radical ideas and feel they warrant consideration, although I don't know right now what side I would come down on). Agadoo seems like a good starting point for the investigation. Of the things mentioned here, the only things we do that seem unusual are keeping so much legacy support, and the Looking through Regarding the use of Regarding semver, one final point and then I won't mention it again here because I don't want to sidetrack the discussion (even more). The reason I am in favor of switching is because we already do use semver. We publish on NPM and it's not possible to do that without using semver, so we're already using it. We just do so wrongly and should fix that if we can since NPM is presumably where the large majority of devs get three.js from these days. |
Regarding Three.Legacy.js... Maybe one way to deal with it would be by only keeping the helper code for 10 releases. If we adopted that, I suspect most of the file (if not all?) would be gone. |
I think we can safely use |
Sounds good to me. There should be definitely a temporal limit for keeping legacy code. 10 releases (which is almost a year) feels long enough. |
|
Alright, let's do it 👍 |
Hm, but why? |
Yes. I'll remove everything below r130. For all other entries in |
I think only the following entries were added there after r130:
|
@mrdoob After merging #24034, Would it be an option to move legacy properties and methods to classes and only keep stuff like |
With
Aside — We are and we aren't... Semver has a special case for versions <1.0.0, where breaking changes are allowed to occur in minor (0.x.0) releases. We're working within that special case. They presumably intended its use for "initial development", whereas we use it for other reasons, but tools like npm and yarn do understand that 0.141.0 → 0.142.0 is a potentially-breaking update. |
That sounds good to me 👍 We'll need to come up with a way to keep track of these legacy properties/methods though... 🤔 |
Possibly just mark them with a |
AFAIK the only reason for choosing I think there are already used things like |
I would also second the ES field declarations suggested by Rich Harris, browser support is good now and can still be transpiled if needed, but that would be for a different issue and a larger discussion across the entire library. Thanks guys for the work on this one, we're getting there! 😉 |
Looks good 👍 |
@Mugen87 bundlers have difficulty handling this case, it will probably not get better in the future. |
I agree with @marcofugaro, I didn't say anything earlier because I'm feeling like I've stirred the pot enough on this topic, but really, we can't have the maintainers of all bundlers change the way their bundlers have been working this whole time, so three.js is tree-shakeable. The bundlers are actually working fine, and work as expected when tree-shaking a flat bundle, or pure ES modules. I am happy we've made improvements with Rollup though. 😄 |
Pure ES modules are the only way to avoid importing the entire library for people using native ES modules in their app with no build tool and hence no tree shaking. Someone who wants to import math classes directly in Node.js, or with native ESM in a browser doesn't want to import the whole library. Another issue is that if the library publishes both a bundle, as well as a Pure ES modules would be the best way to go. |
We will not be changing the NPM entrypoint to source files without a bundling step. I don't believe referring to that option as "pure ES modules" is particularly meaningful. This has been discussed at length before, as you know. |
Fair enough, whatever you want to call it, importing from the source files is side-effect free and works with tree-shaking, compared to the bundle which still has side-effects even after Though all the changes have made a huge difference in my test bundle, reducing the total size down to only 295 KB now (uncompressed), a savings of ~71%! The goal with tree-shaking here is I should be able to import a class from the bundle, and only get the classes needed. In my test bundle importing only We also have agadoo as a way to test. I'm happy to help with troubleshooting the remaining side-effects in the bundle if you guys like with a new issue? 😊 |
@pschroen Yeah, would be good to see what the next steps would be 👍 |
Unclear to what extent this is a change in bundlers, or a misunderstanding of how tree-shaking applies, but the
three.module.js
bundle mostly doesn't get tree-shaken as expected today. Here's a very minimal repro of the issue in Rollup, but we think something similar happens with esbuild and webpack too:Rollup Repl
References:
The text was updated successfully, but these errors were encountered: