This document by Ben Newman, and the ensuing Twitter conversation. You should read those first.
I, Rich Harris, am
- just some guy
- not involved in standards efforts at all
- tbh I don't even work in the technology industry
- but I did write a couple of module bundlers (Esperanto and its successor, Rollup, both of which have had modest success)
- so I do have a dog in this fight
- though Ben is a much smarter programmer/human than me, so he's probably right and I'm probably wrong
Now that's out of the way: Ben has lots of good examples of code that is made cleaner and clearer by allowing import
declarations inside blocks. And this...
To put it in even stronger terms, if we do not allow ourselves to nest
import
declarations, then serious applications will never be able to stop using the CommonJSrequire
function, and JavaScript modules will remain an awkward hybrid of modern and legacy styles.
Is that a future we can tolerate?
...definitely makes you sit up and take notice. But it's not actually true:
if ( weNeedFoo ) {
loader.import( './foo.js' ).then( foo => {
// tada! conditional dynamic loading.
// note: i'm not at all sure that this is
// what it'll look like. will loader be
// called `loader`? will it take a single
// string as its argument? i actually have
// no idea. But you get the general point.
});
}
That's definitely not as pleasant or convenient (or as statically analysable, FWIW) as an import
declaration, but it's pretty clear what's going on here. No surprises about order of execution and whatnot.
Guy suggests that we could treat an inline import
declaration as syntactic sugar:
// this...
if ( weNeedFoo ) {
import foo from './foo.js';
}
// ...is equivalent to this:
if ( weNeedFoo ) {
const foo = await loader.import( './foo.js' );
}
But that's not true, because you can only use await
inside an async function. You could make the whole module async, but then you can't use import
inside a non-async function:
function doFoo () {
import foo from './foo.js';
foo();
}
// doThis and doThat happen in the same tick, which means
// we can't sit around and wait for `foo`
doThis();
doFoo();
doThat();
In Node, that's sort of okay, because (in spite of Noders constantly chastising developers for using synchronous functions where async equivalents exist) require
is synchronous. So we can just use that. But across a network? Nope.
So the implication is that we would make all the modules available (loaded, but not necessarily executed yet) at initial execution time. In other words, for that conditional import
to work in a browser, we'd have to defensively load the entire speculative dependency graph – all the modules that we might need, plus all the modules they might need, and so on – just so foo
could be there when we needed it.
This is how Browserify works, by the way – this code...
const lol = Math.random() < 0.5 ?
require( './foo.js' ) :
require( './bar.js' );
...will cause the contents of both foo.js
and bar.js
to be included in your bundle. Which probably isn't what you wanted.
Maybe we have nested imports, and just get into the habit of not using them (and using loader.import
instead) in code that's intended for the browser. But one of the great things about ES2015 modules is that they will make sharing code between server and client much easier. The only way nested import
declarations really make sense is if we have a node-centric view of the world.
It's entirely possible that I've just completely misunderstood the whole thing, in which case sorry. Like Ben I believe that native modules will have a more significant impact on developer happiness and productivity than any other recent or upcoming JavaScript feature, so it's important we get this stuff right.
Really? A mobile phone on a bad 3G network shouldn't evaluate JavaScript until it's loaded nice-to-have-but-frankly-non-essential stuff like analytics code, or routes that you may or may not visit? I don't agree.
you're a madman :-)
I'd argue that pre-emptively loading everything would be much worse for load times, in typical situations. Truly dynamic imports are rare, but that works in our favour – for
loader.import
calls with string literals, a server could push modules, or a service worker could get busy fetching modules it thinks it might need (depending on what the user ends up doing).