Codesplit WebGL/WebGPU entrypoints
Related issue: #29156
Description
Emits a build where three/webgpu re-exports from core to avoid user configuration or duplication. Each entrypoint will no longer bundle a unique copy of three.js but share a single copy.
To prevent future issues with tree-shaking (#28670), I've code split the common core and code specific to a backend.
// src/Three.core.js -> build/three.core.js -> build/three.core.min.js
export { Vector3 } from './math/Vector3.js';
// window.__THREE__ check for duplication here
// src/Three.js -> build/three.module.js -> build/three.module.min.js -> build/three.cjs
export * from './Three.core.js';
export { WebGLRenderer } from './renderers/WebGLRenderer.js';
// src/Three.WebGPU.js -> build/three.webgpu.js -> build/three.webgpu.min.js
export * from './Three.core.js';
export { WebGPURenderer } from './renderers/webgpu/WebGPURenderer.js';
// src/Three.WebGPU.Nodes.js -> build/three.webgpu.nodes.js -> build/three.webgpu.nodes.min.js
export * from './Three.core.js';
export { WebGPURenderer } from './renderers/webgpu/WebGPURenderer.Nodes.js';
π¦ Bundle size
Full ESM build, minified and gzipped.
| Before | After | Diff | |
|---|---|---|---|
| WebGL | 690.29 171.01 |
338.07 78.63 |
-352.22 kB -92.38 kB |
| WebGPU | 815.35 219.78 |
467.13 129.1 |
-348.22 kB -90.68 kB |
| WebGPU Nodes | 814.86 219.65 |
466.83 129.03 |
-348.03 kB -90.62 kB |
π³ Bundle size after tree-shaking
Minimal build including a renderer, camera, empty scene, and dependencies.
| Before | After | Diff | |
|---|---|---|---|
| WebGL | 463.31 111.89 |
463.4 111.57 |
+93 B -314 B |
| WebGPU | 538.09 145.28 |
538.09 145.28 |
+0 B +0 B |
| WebGPU Nodes | 494.2 135.15 |
494.2 135.15 |
+0 B +0 B |
I think this looks like a good approach! Shared 'core' that the WebGL and WebGPU entrypoints can use, and that doesn't need to be exposed as a separate public entrypoint to users.
(obsolete / resolved issue)
Minor thing, but I'm seeing some duplication of build artifacts when running npm run build locally:
> ls ./build
three.cjs three.module.js three.webgpu.min.js
three.core.min2.js three.module.min.js three.webgpu.nodes.js
three.core2.js three.webgpu.js three.webgpu.nodes.min.js
Should I just remove the build artifacts from this PR or regenerate them?
Removing them is better π .
@mrdoob @sunag Fixing #29156 is crucial for the community so if there are no objections from your side let's merge this so we have a bit of room to r170.
The only workflow that is affected by this change is when devs copy builds around since there is three.core.js now. However, this is not a workflow that we promote in our installation guide (1. npm/build tool, 2. CDN). So I think this is good to go.
I like the changes π
I tried this PR on a large-scale project, and overall, it works well. Auto-complete functions correctly, TypeScript issues are resolved, and the only refactoring I had to perform was updating imports to use three/webgpu for WebGPU-specific code (similar to how we previously handled three/tsl).
For example:
import { Matrix4, WebGPURenderer, StorageInstancedBufferAttribute } from 'three';
// becomes -->
import { Matrix4 } from 'three';
import { WebGPURenderer, StorageInstancedBufferAttribute } from 'three/webgpu';
Which makes perfect sense.
However, this PR highlighted an existing issue, in most case addons code that relies on ./build/three.webgpu.js won't be resolved out-of-the-box in developers' projects. For example, when using Vite, the following error occurs:
β [ERROR] No matching export in "../node_modules/.pnpm/three@file+frontend+three-0.170.0-dev.tgz/node_modules/three/build/three.module.js" for import "Line2NodeMaterial"
../node_modules/.pnpm/three@file+frontend+three-0.170.0-dev.tgz/node_modules/three/examples/jsm/lines/webgpu/LineSegments2.js:12:1:
12 β Line2NodeMaterial
β΅ ~~~~~~~~~~~~~~~~~
β [ERROR] No matching export in "../node_modules/.pnpm/three@file+frontend+three-0.170.0-dev.tgz/node_modules/three/build/three.module.js" for import "Line2NodeMaterial"
../node_modules/.pnpm/three@file+frontend+three-0.170.0-dev.tgz/node_modules/three/examples/jsm/lines/webgpu/Line2.js:4:9:
4 β import { Line2NodeMaterial } from 'three';
Only a small portion of addon code relies on the WebGPU build. We just need to notify that these codes are part of the webgpu build through correct imports. I will submit a PR with a patch so we can merge it after this one.
Other than that, everything is good to go, and working with the WebGPURenderer feels even more amazing now, especially with auto-complete and TypeScript!
Well, seems like we all agree to move on with this? Who's gonna take the lead and merge this (and #29644 next)? @Mugen87 @sunag π
I was just starting to build a collection here.
Well, seems like we all agree to move on with this? Who's gonna take the lead and merge this (and https://github.com/mrdoob/three.js/pull/29644 next)?
Sorry to ask this a little late, but can't we merge the class structure and then think about splitting the imports in the examples later?
The syntax below changes a lot to be used as main in the examples, I haven't been able to dedicate myself to it yet to analyze other possibilities.
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu'
The syntax below changes a lot to be used as main in the examples, I haven't been able to dedicate myself to it yet to analyze other possibilities.
import * as THREE from 'three'; import { WebGPURenderer } from 'three/webgpu'
Maybe I didn't clearly explain my point in the previous message. This PR doesn't change the imports in the examples, as we're still manually overriding the import map with "three": "../build/three.webgpu.js", which is essentially a workaround. So, the syntax:
<script type="importmap">
{
"imports": {
"three": "../build/three.webgpu.js"
}
}
</script>
import * as THREE from 'three';
const renderer = THREE.WebGPURenderer()
continues to function without any issues, as seen in this PR, since none of the Puppeteer examples break. /cc @sunag
What I intended to highlight is that with this PR, we now have the option to use the correct imports for WebGPU without relying on the "three": "../build/three.webgpu.js" override, allowing us to simplify the import structure going forward.
Well, seems like we all agree to move on with this? Who's gonna take the lead and merge this (and https://github.com/mrdoob/three.js/pull/29644 next)?
@mrdoob should definitely approve this change. The type of builds and the import structure is a fundamental aspect of the library and I'm not sure how he feels about this approach.
I understand the PR is a great solution for our current promoted workflows (npm, CDN) but it moves away from the idea to have a renderer with its core classes in a single self-contained build file. Regarding #29644 I'm not sure it's a good user experience to have three, three/webgpu and three/tsl. Hopefully we can merge three/webgpu and three/tsl somehow so the imports get not too complicated for users.
If you really need a compromise, we can bundle everything into the minified three.module.min.js and three.webgpu.min.js so they are self-contained, but continue code-splitting for non-minified bundles which NPM will accept. That assumes you'd only use one minified bundle at a time, as Renaud indicated for examples. I'm personally not a fan of example code or imports themselves diverging too much based on environment (CDN, bundler), but that is a casualty of this approach or effective workaround.
<script type="importmap">
{
"imports": {
"three": "../build/three.webgpu.min.js"
}
}
</script>
import * as THREE from 'three';
const renderer = THREE.WebGPURenderer();
// npm i three
import * as THREE from 'three/webgpu';
const renderer = THREE.WebGPURenderer();
It seems the examples can't be properly debugged and inspected if the builds are minified. Ideally, they use an unminified bundle.
That said, I'm personally fine with the initial approach you have implemented. I'm just curious to see how @mrdoob evaluates the PR and the need for self-contained bundles.
Is there a reason we don't emit sourcemaps? That would have errors in all bundles point back to source code and allow tracing in Lighthouse.
@CodyJasonBennett just checking you're aware that the sourcemaps are currently built with yarn dev but not yarn build βΒ Are you asking about building and publishing sourcemaps to npm as well?
@mrdoob should definitely approve this change. The type of builds and the import structure is a fundamental aspect of the library and I'm not sure how he feels about this approach.
That said, I'm personally fine with the initial approach you have implemented. I'm just curious to see how @mrdoob evaluates the PR and the need for self-contained bundles.
Gentle ping @mrdoob π. This PR has received approval from most collaborators, but before proceeding, we would really appreciate your input. The import structure and build types are critical to the library's architecture, and we'd like to understand your perspective on the approach:
Self contained:
// npm i three
import * as THREE from 'three/webgpu';
const renderer = THREE.WebGPURenderer();
Or not:
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu'
This update will still require people to update their build setups (or importmaps). The idea in https://github.com/mrdoob/three.js/issues/29156#issuecomment-2457694632 would fully avoid the requirement for build setups to be updated, while providing optional ways to achieve the same goals as this new three/webgpu import path. That alternative would make ecosystem migration easier. Updating a version number and using new APIs is inevitable, but having to also update build setups is a hassle that could be avoided.
I elaborated in https://github.com/mrdoob/three.js/issues/29156#issuecomment-2458239359, but this is a gross misdiagnosis.
The src code split sounds good to me π
The naming is becoming weird though.
This would look better to me:
src/Constants.js
src/Core.js
src/Legacy.js
src/Three.js // do we still need this one?
src/Utils.js
src/WebGL.js
src/WebGPU.js
The builds, I'm not so sure...
What do you guys think of this?
builds/three.core.js
builds/three.webgl.js
builds/three.webgpu.js
builds/three.module.js
three.core.js: Core three.webgl.js: WebGLRenderer only three.webgpu.js: WebGPURenderer only (And NodeMaterials only) three.module.js: Core + WebGLRenderer + WebGPURenderer
@mrdoob For src/ files, I like your suggestion. For build/ files, I would think of it in terms of package.json#exports entrypoints. It is probably too soon for the three entrypoint to include WebGPURenderer. Something like this I would support...
-
three-> Core + WebGLRenderer -
three/webgpu-> Core + WebGPURenderer + Nodes
... which could be implemented on top of build/three.core.js, build/three.webgl.js, and build/three.webgpu.js bundles, similar to what's in this PR but renamed slightly. Optionally there could be additional three/core, three/webgl, and three/tsl entrypoints... I don't feel strongly about any of those others at the moment.
It is probably too soon for the three entrypoint to include WebGPURenderer. Something like this I would support...
Having used WebGPURenderer across all my projects for over a year, I fully agree with the concerns. Given the WebGPU API's ongoing instability, it seems premature to push this onto most Three.js developers, especially since many Three.js developers may not adopt WebGPURenderer for some time. Adding it now could undermine previous efforts to optimize bundle size through tree-shaking:
(see https://github.com/mrdoob/three.js/pull/29827)
A structure like this strikes a balanced approach:
three -> Core + WebGLRenderer three/webgpu -> Core + WebGPURenderer + Nodes
This setup respects the longevity of WebGLRenderer, which is likely to remain in widespread use, while also providing a clear, separate path for WebGPURenderer. Merging both renderers into the same module would likely add unnecessary complexity, with WebGLRenderer and WebGLBackend coexisting in a confusing, overlapping space.
Thereβs no real benefit in bundling both WebGLRenderer and WebGPURenderer together in the same app, as they canβt communicate with each other. Furthermore simply switching WebGLRenderer to WebGPURenderer isnβt feasible either, given that the material systems are fundamentally different.
I think the naming suggestion is a good one, as the example Core.js instead of Three.core.js.
Merging the two renderers into a single module could limit flexibility in the creation process. For example, in cases like PMREMGenerator, where we kept the same API/interface but internally moved from GLSL to TSL, this could pose challenges if we need to do it in other core classes. It may also make it more difficult to simply swap the renderer from WebGLRenderer to WebGPURenderer, along with other concerns already mentioned.
I would suggest moving forward with the renaming and merging the PR as proposed, and addressing this additional issue later if needed.
I would suggest moving forward with the renaming and merging the PR as proposed, and addressing this additional issue later if needed.
Absolutely. What has been suggested in https://github.com/mrdoob/three.js/pull/29404#issuecomment-2460102908 sounds good to me as well.
https://github.com/mrdoob/three.js/pull/29404#issuecomment-2460102908 sounds good to me too π
Nice π! Then I think we can merge this PR as it is? /cc @Mugen87
Yeah, let's start with this configuration. We can apply further updates with additional PRs if necessary.
π¦ Bundle size
Full ESM build, minified and gzipped.
Before After Diff WebGL 690.29
171.01338.07
78.63-352.22 kB
-92.38 kB
Hmm, how come the minified build is now -92.38 kB less? π€
I could imagine the script still measures the sizes based on a single build file and does not honor three.core so far.
Ah, got it. I'll need to update that table again then...