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

WebGPURenderer: Fix global references in Node.js #29919

Merged
merged 6 commits into from
Nov 24, 2024

Conversation

whatisor
Copy link
Contributor

@whatisor whatisor commented Nov 19, 2024

Fix #29916.

Description
Fix compilation issue on react.

Copy link

github-actions bot commented Nov 19, 2024

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 339.13
79
339.13
79
+0 B
+0 B
WebGPU 484.71
134.2
484.87
134.24
+156 B
+45 B
WebGPU Nodes 484.18
134.1
484.34
134.14
+156 B
+44 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 464.59
111.96
464.59
111.96
+0 B
+0 B
WebGPU 552.01
149.42
552.17
149.47
+156 B
+44 B
WebGPU Nodes 507.89
139.13
508.05
139.18
+156 B
+44 B

@sunag sunag changed the title Fix compilation in react WebGPURenderer: Fix compilation in react Nov 19, 2024
@sunag sunag added this to the r171 milestone Nov 19, 2024
@donmccurdy donmccurdy changed the title WebGPURenderer: Fix compilation in react WebGPURenderer: Fix global references in Node.js Nov 19, 2024
@donmccurdy
Copy link
Collaborator

Note – I've renamed the issue to "fix global references in Node.js". I think that's the issue here, not React. Next.js is a React-based framework that happens to execute code in both a browser environment and server-side in Node.js.

@@ -64,7 +64,7 @@ class WebGPUBackend extends Backend {
powerPreference: parameters.powerPreference
};

const adapter = await navigator.gpu.requestAdapter( adapterOptions );
const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : {};
Copy link
Collaborator

@Mugen87 Mugen87 Nov 19, 2024

Choose a reason for hiding this comment

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

@donmccurdy{} is intended as a workaround for node but I'm not sure {} is ideal here. Do you think this use case is handled appropriately or would you implement it in a different way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Base on that function code, it should be null, otherwise, next step is exceptional?
"adapter.features"

Copy link
Collaborator

@donmccurdy donmccurdy Nov 19, 2024

Choose a reason for hiding this comment

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

@whatisor are you executing this code in the Node.js environment and seeing runtime errors, or is this a compile-time error (like from TypeScript or Webpack)? Not clear to me from #29916.

I'm not sure that backend.init( renderer ) should be making efforts to avoid runtime exceptions, if running in an environment where neither WebGL nor WebGPU exists. I would support fixes that avoid build-time errors, and run-time errors in module scope. But if you are initializing and using a renderer in Node.js, a runtime exception is to be expected.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure that backend.init( renderer ) should be making efforts to avoid runtime exceptions, if running in an environment where neither WebGL nor WebGPU exists

I agree, I think we should be throwing an error here, it doesn't make much sense to me to proceed with an empty object as an adapter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@donmccurdy it is compile error.
We also need to fix three\examples\jsm\capabilities\WebGPU.js with global and await.
So, we should return null and (exception is next step as current)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

three\examples\jsm\capabilities\WebGPU.js

Capabilities should be optional, it should be possible to use WebGPURenderer without global await. #29218

Copy link
Contributor Author

@whatisor whatisor Nov 20, 2024

Choose a reason for hiding this comment

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

can we do same thing with webgl to avoid await?
https://developer.mozilla.org/en-US/docs/Web/API/GPUCanvasContext

=> No, we still need to await for real adapter. I guess browser need to improve this part.

Use `null` as fallback when navigator isn't available.
@@ -113,13 +113,21 @@ class WebGPUUtils {
// TODO: Remove this check when Quest 34.5 is out
// https://github.com/mrdoob/three.js/pull/29221/files#r1731833949

if ( navigator.userAgent.includes( 'Quest' ) ) {
if ( typeof navigator !== 'undefined' ) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we remove this? It is inside a function that will never be executed by the adapter return null in this case.

Copy link
Collaborator

@donmccurdy donmccurdy Nov 20, 2024

Choose a reason for hiding this comment

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

I guess the issue is that it's a compiler error, not a runtime error? I wish we knew what in the Next.js stack is doing compile-time checks on dependencies, and what its requirements are. Avoiding all references to browser globals is a pretty tall order for a large library relying heavily on Web APIs. :/

Copy link
Collaborator

Choose a reason for hiding this comment

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

The compiler throwing an error for a global variable inside a function seems strange to say the least, since a variable can be declared at the global level after the function declaration.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I'm not sure why... trying to reproduce the issue:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@verekia
Copy link

verekia commented Nov 21, 2024

I have set up a test suite of various build environments, like Vite, with and without React, Next.js, R3F, and a specific one to check this PR: https://github.com/verekia/three-gpu-ecosystem-tests

The PR does indeed fix Next.js crashing on reference errors 🙌 That being said, I am getting a Multiple instances of Three.js being imported warning, which I don't get when importing WebGPURenderer dynamically. Maybe @CodyJasonBennett would have insights on why that happens.

Regular import (fixed by this PR):
next-pages-vanilla-pr-29919/pages/index.js

Dynamic import workaround:
next-pages-vanilla-dynamic-8ce69e0/pages/index.js

@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 23, 2024

It seems the only blocking bit is the change in WebGPUUtils.

@verekia Can you please verify if build works without this change? If so, we can revert it ( see #29919 (comment)).

@verekia
Copy link

verekia commented Nov 24, 2024

Yes, it works without the WebGPUUtils change. I added the test case to my repo.

@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 24, 2024

Thanks for letting us know! I have reverted the changes in WebGPUUtils so it seems the PR is ready to go.

@Mugen87 Mugen87 merged commit 927c20c into mrdoob:dev Nov 24, 2024
12 checks passed
@whatisor
Copy link
Contributor Author

Thanks for letting us know! I have reverted the changes in WebGPUUtils so it seems the PR is ready to go.
@Mugen87 We still need to update WebGPUtils and examples because people stills use it from example and it will not be able to compile, don't we?

@verekia
Copy link

verekia commented Nov 24, 2024

@whatisor Could you provide a repro where WebGPUUtils causes a crash in a Next.js project?

My test is pretty basic, the only thing I import is:

import * as THREE from 'three'
import { WebGPURenderer } from 'three/webgpu'

@CodyJasonBennett
Copy link
Contributor

This PR has nothing to do with React or its strict mode etc. We need a reproduction to know what's going on here or if this is something we can actually support. Next is notorious for overzealous compilation the ecosystem itself can't mitigate.

@whatisor
Copy link
Contributor Author

@whatisor Could you provide a repro where WebGPUUtils causes a crash in a Next.js project?

My test is pretty basic, the only thing I import is:

import * as THREE from 'three'
import { WebGPURenderer } from 'three/webgpu'

I did not mean issue of Core. It is just simply call Webgpu.isAvailable() in your test to see compilation issue, but I will push PR to your test case

@verekia
Copy link

verekia commented Nov 25, 2024

@whatisor You are not supposed to call any method that's meant for browsers during SSR. These should live in a useEffect or similar. See this classic article about hydration.

WebGPU.isAvailable() // ❌ Don't do that, it runs on the server during SSR

function MyComponent() {
  WebGPU.isAvailable() // ❌ Don't do that, it runs on the server during SSR

  useEffect(() => {
    WebGPU.isAvailable() // ✅ No problem, runs only in the browser
  }, [])

  return // ...
}

That's why the only concern for Three.js should be that importing in a Next.js project doesn't cause a crash due to global browser variable access and top-level browser API calls. If you call methods during SSR that should never run on the server, then it's expected for SSR to break.

@verekia
Copy link

verekia commented Nov 25, 2024

I added more test cases to my repo, including Next 14/15, React 18/19, Pages/App router, and a TSL call for all cases. Now that this PR is merged, things are looking really good for Next.js support in r171. Thank you.

  • ✅ Vite + vanilla Three.js: Works in all cases.
  • ✅ Next.js + vanilla Three.js: Works in all cases.
  • ✅ Vite + R3F: Works.
  • ✅ Next.js 14 + R3F: Works in both Pages and App routers.
  • ❌ Next.js 15 + R3F: Does not work, except with Pages Router + React 18.

The recent release of Next.js 15 + React 19 RC is expected to break things on the R3F side, so it's not a problem Three.js should worry about.

@Mugen87 Mugen87 mentioned this pull request Nov 25, 2024
@whatisor
Copy link
Contributor Author

  useEffect(() => {
    WebGPU.isAvailable() // ✅ No problem, runs only in the browser
  }, [])

I added into your 2 test cases and it is failed from BUILD step.
image

Thank @Mugen87 because of removing self but still issue with nagivator and warning with await.

@whatisor
Copy link
Contributor Author

@verekia
Copy link

verekia commented Nov 25, 2024

You're right @whatisor, this navigator is at the top-level, so it will break imports. The one we reverted in WebGPUUtils was not top-level, as it was in a class method. Let's keep hunting down only the top-level ones :)

@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 25, 2024

I guess we can simply do the following?

let isAvailable = ( typeof navigator !== 'undefined'  && navigator.gpu !== undefined );

It does not matter that isAvailable ends up false, right? This is just a compile issue.

@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 25, 2024

and warning with await.

How does this warning look like? Is it still possible to compile your code with the below await usage?

isAvailable = await navigator.gpu.requestAdapter();

@verekia
Copy link

verekia commented Nov 25, 2024

It compiles with this warning:

   ▲ Next.js 15.0.3

 ✓ Linting and checking validity of types    
   Creating an optimized production build ...
 ⚠ Compiled with warnings

./node_modules/three/examples/jsm/capabilities/WebGPU.js
The generated code contains 'async/await' because this module is using "topLevelAwait".
However, your target environment does not appear to support 'async/await'.
As a result, the code may not run as expected or may cause runtime errors.

Import trace for requested module:
./node_modules/three/examples/jsm/capabilities/WebGPU.js

 ✓ Compiled successfully

I am on Node v22.10.0, and interestingly, I was not getting the ReferenceError because navigator is actually defined in that version. I switched to Node 18 and it's not defined. Anyway, I think it's a good practice to do these typeof checks for navigator, window, and other global browser objects.

@Mugen87
Copy link
Collaborator

Mugen87 commented Nov 25, 2024

I think we can live with that warning for now.

@verekia
Copy link

verekia commented Nov 26, 2024

navigator was added in Node 21

@verekia
Copy link

verekia commented Nov 26, 2024

After another full day of testing and Dockerizing for more reliable reproductions, I have updated the repo with new findings. I have added an import of capabilities/WebGPU.js and call to isAvailable in all cases.

The main takeaway on the Three.js side is that importing top-level await modules such as capabilities/WebGPU.js does break Vite's default config, and causes that warning we discussed above in Next.js, both in the browser console and at compilation.

You can test it easily:

  • cd vite-vanilla-js
  • Comment out optimizeDeps and build.target in vite.config.js
  • npm run docker (or npm i && npm start if you don't have Docker)

Or Next.js:

  • cd next15-pages-vanilla-react19
  • npm run docker (or npm i && npm start)

@whatisor
Copy link
Contributor Author

Yeah, I still need to implement my own WebGPU async checking anyway. Because cannot risky update everything to latest.

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.

Improve compatibility of three/webgpu with nextjs
6 participants