Importing three dynamically

hi all,

I need to load three via cdn only when and if necessary, so I wanted to do this :

const promise = new Promise( ( resolve, reject ) => {
	const script = document.createElement( 'script' );
	script.type = 'module';
	script.src = 'https://cdn.skypack.dev/three@0.130.0/build/three.module.js';
	script.addEventListener( 'load', resolve );
	script.addEventListener( 'error', e => reject( e.error ) );
	document.head.appendChild( script );
} );

But I found three is no longer referenced in window.THREE, so I have no way to access the loaded lib outside of the script tag…

Do you have an alternative solution ?

Dynamic imports are supported in most browsers now.

https://caniuse.com/es6-module-dynamic-import

const module = await import('/modules/my-module.js');
4 Likes

small addition, we need to import three and everything else from either skypack or unpkg, including 3rd party packages,

const THREE = await import('https://cdn.skypack.dev/three@0.130.0')
const { OrbitControls } = await import('https://cdn.skypack.dev/three@0.130.0/examples/jsm/controls/OrbitControls.js)

then it works fine. we can’t mix a local three with remote packages, or target a specific entry file (build/three.module.js) or else there will be multiple three’s and namespaces. but then it will work fine. :+1:

1 Like

@looeee @drcmda Thanks both of you for the information, but it seems dynamic import is not fully supported by Webpack…

I’m running into this error :
The target environment doesn't support dynamic import() syntax so it's not possible to use external type 'module' within a script.

I filed an issue to dicuss referencing three in global scope : reference three in global scope · Issue #22295 · mrdoob/three.js · GitHub

so you’re trying to load an mjs or “browser esm” with dynamic imports? if you’re using a bundler, imo you shouldn’t have script tags in your application, nor externals. you just import/dynamic import locals and/or npm modules from index.js.

what i usually do is make one module for everything that has to do with the canvas, then i dynamic import it into the root app. there won’t be any issues with that, nor is it dependent on systems supporting dynamic imports or not. usually people also combine this with routes, every route is a dynamic import.

index.js

...
const { ... } = await import ('./canvas)

canvas.js

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

export { ... }

you normally don’t add any of these to index.html, the bundler should do that for you. or at least it only has the main entry (index.js).

1 Like

Bsaed on what is written in webpack’s documentation there are two ways of resolving dynamic import:

Two similar techniques are supported by webpack when it comes to dynamic code splitting. The first and recommended approach is to use the import() syntax that conforms to the ECMAScript proposal for dynamic imports. The legacy, webpack-specific approach is to use require.ensure . Let’s try using the first of these two approaches…

I suggest you to update your webpack, if this solution works for you:

1 Like

@gorskidev @drcmda thanks again for all this information ! In my case all my app including three is loaded conditionally from a tiny script like the Google Analytics snippet you paste in <head>, it is the first time I do that so I’m a bit lost.

I found that my original attempt of loading three dynamically via a script tag ( which is still the only method supported by all browsers ) didn’t work only with skypack. Doing the same with an unpkg url works fine, I can find three in window.THREE. Go figure…

So the working code is :

const promise = new Promise( ( resolve, reject ) => {
	const script = document.createElement( 'script' );
	script.src = 'https://unpkg.com/three@0.131.0/build/three.js';
	script.addEventListener( 'load', resolve );
	script.addEventListener( 'error', e => reject( e.error ) );
	document.head.appendChild( script );
} );

promise
.then( () => {
	// window.THREE is here
} )
.catch( error => {
	console.error( error );
} );

For those interested :

Fiddle of the failed attempt with skypack : https://jsfiddle.net/felixmariotto/5bntcaxz/6/
Fiddle of the successfull attempt with unpkg : https://jsfiddle.net/felixmariotto/5bntcaxz/5/

2 Likes

hmm, it seems a little fragile. this is a 1.6 megabyte bomb through a remote cdn bundler, it will 100% have service fallouts in prod. it’s not a module but the umd, if you have use additional modules (three/jsm) you’ll end up with two or more threejs. for unpkg you normally need the ?module postfix: https://unpkg.com/three@0.131.0?module

which is still the only method supported by all browsers

you are right, but browser esm is not real, it can’t be used in browsers without bundlers (webpack, skypack, unpkg, etc), you need something that resolves dependencies. if you introduce browser semantics (script tags), it will adverse effects. only a suggestion ofc. :blush:

1 Like

I’m doing something like this on Discoverthreejs.com to make the page interactive as fast as possible.
I created two bundles:

  • chapterEarly.js: extremely lightweight (3.7kb before gzip), just enough to get the page interactive
  • chapterLate.js: all the heavy lifting goes here, setting up the editor, examples and so on. Three.js, OrbitControls, etc. are all bundled in here

Then I load them like this:

<head>
    <script type="module>
        import { setupChapterEarly } from "js/chapterEarly.js";
        setupChapterEarly();

        window.addEventListener('load', async () => {
            const { setupChapterLate } = await import("/js/chapterLate.js");
            setupChapterLate();
        });
    <script>
<head>

This moderately improved Lighthouse scores but more importantly it made the page interactive way faster - you can interact with the text pretty much instantly even when the network is throttled to 2G speeds.

2 Likes