Skip to content

Instantly share code, notes, and snippets.

@Rich-Harris
Created June 10, 2016 18:15
Show Gist options
  • Save Rich-Harris/3107038d3d3f9ed956a1ac9fd6e75b97 to your computer and use it in GitHub Desktop.
Save Rich-Harris/3107038d3d3f9ed956a1ac9fd6e75b97 to your computer and use it in GitHub Desktop.

The case against nested import declarations

Context

This document by Ben Newman, and the ensuing Twitter conversation. You should read those first.

Who am I?

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 CommonJS require 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.

@trusktr
Copy link

trusktr commented Jun 24, 2016

How does the loader load modules simultaneously when possible? Maybe imports within the same block,

if (something) {
  import { a } from "./a";
  import { b } from "./b";
  import { c } from "./c";
}

desugars to something like

if (something) {
  const __modules__ = await loader.import('./a', './b', './c')
  const {a} = __modules__['./a']
  const {b} = __modules__['./b']
  const {c} = __modules__['./c']
}

so that the loader can determine the most efficient way to load the modules as simultaneously as possible?

@benjamn
Copy link

benjamn commented Jun 27, 2016

@trusktr I think the idiomatic desugaring would be

if (something) {
  const [a, b, c] = await Promise.all(
    loader.import("./a"),
    loader.import("./b"),
    loader.import("./c")
  );
}

I definitely agree with you that sequential asynchronous operations seem inefficient, but I'm not entirely sure it's safe to parallelize module evaluation. What you really want to parallelize is the fetching of the module sources, and then the imports themselves can be sequential and synchronous.

@Rich-Harris
Copy link
Author

@benjamn @trusktr You're right, it's definitely not safe to parallelize evaluation, and by extension I think the Promise.all form is a non-starter.

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