Skip to content
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

Get rid of build step #5363

Open
dmarcos opened this issue Oct 16, 2023 · 19 comments
Open

Get rid of build step #5363

dmarcos opened this issue Oct 16, 2023 · 19 comments

Comments

@dmarcos
Copy link
Member

dmarcos commented Oct 16, 2023

The post below made me consider it for the first time:

https://world.hey.com/dhh/you-can-t-get-faster-than-no-build-7a44131c

Has the time come to modularize components and get rid of the build step?

@mrxz
Copy link
Contributor

mrxz commented Oct 17, 2023

As appealing as having no build step is, it doesn't make sense to have it as a goal in and of itself. The real question becomes what would be gained by eliminating the build step? Modularization (of components) is something that can be achieved in numerous ways so I'd argue is orthogonal to the decision whether or not to get rid of the build step.

Faster "builds" while developing A-Frame itself would be the most prominent benefit. Given the relatively small code base of A-Frame, the builds don't seem particularly slow (especially incremental builds) and there are probably ways to improve it still within the given build setup.

It's worth noting that many users of A-Frame already have "no build step" setups, as they simply include the A-Frame bundle and are good to go. They stand little to gain and depending on how it's handled, might even have to do more to get things going, for example adding an import map + polyfill.

Not saying this shouldn't be done, but I feel that there's more value in the modularization than the elimination of the build step. When done right it would enable tree-shaking and make it easier to integrate A-Frame with other frameworks and build tools. For backwards-compatibility a complete (synchronous) bundle can still be provided.

@dmarcos
Copy link
Member Author

dmarcos commented Oct 17, 2023

Thanks for the feedback. No build is a step towards eliminating unnecessary tooling and complexity that makes programming less enjoyable. I agree that modularization and build step are separate goals with different benefits.

Anyone is more than welcome to take a stab and modularization without adding extra tooling and complexity. I'm intrigued to see how it would look like. Main goal is always to make A-Frame and programming more fun 😀

@vincentfretin
Copy link
Contributor

I think you still need a minifier, non minified build is huge.
2.6 MB aframe-v1.4.2.js
1.3 MB aframe-v1.4.2.min.js

If you want an unminified aframe without build step, first step would be to use ES6 modules, modify all the require and module.exports by import /export.

@dmarcos
Copy link
Member Author

dmarcos commented Oct 17, 2023

If you want an unminified aframe without build step, first step would be to use ES6 modules, modify all the require and module.exports by import /export.

Would be nice to see how it looks like. My main concern about dev experience would be backwards compat with "non modularized" components or that one cannot longer just load a component via a simple script tag.

@mrxz
Copy link
Contributor

mrxz commented Feb 17, 2024

Did some experimentation with replacing all require and module.exports with import/export and it's doable. Mostly interested to see if tree-shaking could yield interesting results and for the hello-world scene I was able to bring it down to a ~600k minified (non-gzipped) bundle, omitting large chunks of Three.js and unused components/systems. Probably could be brought down further by narrowing down the imports to only the needed parts.

For no build-step, there is another hurdle, some of the dependencies used aren't ESM. These include the debug, deep-assign, webvr-polyfill and load-bmfont/three-bmfont-text/buffer libraries.

As for compatibility, it's possible to still output a bundle that behaves the same, and can be crudely imported using import 'aframe' in an ESM context (pending #5419). Alternatively users could opt to include selective parts by importing only those systems/components import 'aframe/components/hand-tracking-grab-controls.js' which will in turn include all dependent components/systems. For third-party components it depends if they do the same, in which case it works neatly, or they 'expect' certain components to be present. So you either get something like:

import 'aframe'; // Full A-Frame setup

import 'third-party-esm-component';
import 'older-plain-component';

Or

import 'aframe/base'; // Minimal 
import 'aframe/components/hand-tracking-grab-controls';

// Pulls in it's own dependencies from aframe/...
import 'third-party-esm-component'; 

// Assumes certain dependencies to be present, so import them manually
import 'aframe/components/link';
import 'older-plain-component';

But this can all be incremental improved. As a first step the switch to ES6 modules can be made, while still retaining the same bundle outputs. From there steps can be taken towards 'no build', 'three-shaking', etc...

@dmarcos
Copy link
Member Author

dmarcos commented Feb 17, 2024

Cool thanks. As soon as we can still keep an A-Frame build around that can load via a single script tag I'm good to explore

@vincentfretin
Copy link
Contributor

When working on #5522 I saw that the tests are currently using process.nextTick that need the process.browser polyfill so currently the tests need a build step with karma-webpack. I think those tests could be rewritten to use setTimeout instead of nextTick so we can get rid of the process.browser polyfill in tests/karma.conf.js. It will be a step further.

@vincentfretin
Copy link
Contributor

vincentfretin commented Jan 16, 2025

In a webpack context, I am using for example:

import "aframe/src/index.js";
import "aframe-extras/controls/index.js";
import { ParametricGeometry } from "three/addons/geometries/ParametricGeometry.js";

so that's easy to bundle my own three version and be sure to only have a single version of three loaded.

To have that without a bundler, with an importmap that would be:

    <script type="importmap">
      {
        "imports": {
          "aframe": "https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-master.module.min.js",
          "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
          "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
        }
      }
    </script>
    <script type="module">
      import AFRAME from "aframe";
      import * as THREE from "three";
      import { ParametricGeometry } from "three/addons/geometries/ParametricGeometry.js";
    </script>

That's currently not possible, but it would be really nice to have that.
For me that would be the main selling point to move all the requires to import/export.
This avoids maintenance burden of creating an cjs bundle with THREE global for each three/addons file you want to use.

So we keep a build step and also create aframe-master.module.min.js without including super-three in it. That's what I suggest.
aframe-master.min.js still includes the super-three code.

@coderofsalvation
Copy link

exciting solution @vincentfretin
I mean its really explicit & flexible regarding versions.
Potentially it could also make broken/outdated aframe apps easier to fix/upgrade/maintain/extend (for non-experts).

@vincentfretin
Copy link
Contributor

Another thing to keep in mind, as of three r172, to use the webgpu render or tsl, you import three.webgpu.js instead of three.module.js (currently aframe bundle includes three.module.js)

That's the importmap used in webgpu examples https://threejs.org/examples/?q=webgpu

                <script type="importmap">
			{
				"imports": {
					"three": "../build/three.webgpu.js",
					"three/webgpu": "../build/three.webgpu.js",
					"three/tsl": "../build/three.tsl.js",
					"three/addons/": "./jsm/"
				}
			}
		</script>

and use WebGPURenderer instead of WebGLRenderer of course, not sure how we can switch aframe renderer to use one or the other if the three inclusion is handled by the user.

@vincentfretin
Copy link
Contributor

Anyway first step is to replace all require and module.exports with import/export. @mrxz do you have a local branch of your previous experiment or do we need to do it again?

@mrxz
Copy link
Contributor

mrxz commented Jan 17, 2025

@vincentfretin I've dusted off my local branch, rebased it and fixed the unit tests. The result can be seen in this PR #5632

@vincentfretin
Copy link
Contributor

Awesome, I'll take a closer look soon at your work and do some experiments.

@vincentfretin
Copy link
Contributor

vincentfretin commented Jan 19, 2025

It doesn't make sense to me to have no build in aframe repo, that would mean the user need to set themself in the importmap the other dependencies buffer, debug, deep-assign, load-bmfont, super-animejs, three-bmfont-text. That's not user friendly. And some dependencies are still cjs, so not feasible currently anyway.

No build for creating an experience with an importmap with aframe, three and additional three/addons without the need to know how to create a cjs bundle of a three addon file just to be able to use it, that yes that's the use case we're looking for in my opinion.

@vincentfretin
Copy link
Contributor

About WebGPURenderer, we could have a property on the renderer to select between webgl and webgpu renderer, but that would work only with importmap.
When you use in the importmap:

 <script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.webgpu.js"
  }
}
</script>

THREE.WebGLRenderer is undefined, THREE.WebGPURenderer is defined.
If you use three.module.js, that's the inverse.

I did a quick test with replacing THREE.WebGLRenderer by THREE.WebGPURenderer, commenting the renderer.xr.setPoseTarget and renderer.xr.setFoveation lines, I get an infinite

three.webgpu.js:1642 Uncaught TypeError: childNode.build is not a function
    at Proxy.build (three.webgpu.js:1642:17)

I have no idea what's going on :D
Anyway I won't investigate that myself, I don't even have a machine with WebGPU enabled, I only have Ubuntu. Log says:
THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.

@vincentfretin
Copy link
Contributor

I made an example using aframe with importmap and using the teapot geometry from three/addons:
https://vfretin-aframe-importmap.glitch.me/
No "Multiple instances of Three.js being imported." warning!

@coderofsalvation
Copy link

awesome. WDYT so far @dmarcos ?

@vincentfretin
Copy link
Contributor

About WebGPURenderer, we could have a property on the renderer to select between webgl and webgpu renderer, but that would work only with importmap. When you use in the importmap:

<script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.webgpu.js" } } </script>

THREE.WebGLRenderer is undefined, THREE.WebGPURenderer is defined. If you use three.module.js, that's the inverse.

I did a quick test with replacing THREE.WebGLRenderer by THREE.WebGPURenderer, commenting the renderer.xr.setPoseTarget and renderer.xr.setFoveation lines, I get an infinite

three.webgpu.js:1642 Uncaught TypeError: childNode.build is not a function
    at Proxy.build (three.webgpu.js:1642:17)

I have no idea what's going on :D Anyway I won't investigate that myself, I don't even have a machine with WebGPU enabled, I only have Ubuntu. Log says: THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.

The infinite loop was caused by some sort of algorithm conflict with the isNode check, my guess is because we set object3D.el that has isNode to true, renaming it to isAframeNode fixed the issue.
If someone want to play with WebGPURenderer and TSL, #5655 has the minimal changes to make it work.

@vincentfretin
Copy link
Contributor

Note that setFoveation was fixed in r173 it seems, I read that WebGPURenderer is now using the new XRManager. Only setPoseTarget is missing, it's a super-three patch.

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

No branches or pull requests

4 participants