> From my point of view, the goal posts that keep moving are the ones put up by anti-JS-modules folk because they keep comparing unbundled JS modules vs bundled CJS. Unbundled CJS would perform far worse than standard JS modules by all these measures, but somehow gets a pass because it has to be bundled.
A feature that by default encourages use that is both slower and less secure on the web is a bad feature, full stop. The SRI problem is not even solved yet. Shipping import statements in production leads to slower websites. This is exacerbated by the fact that the syntax looks synchronous but behaves asynchronously. As someone who clearly cares much more about the browser environment than the node environment, these problems should resonate with you, regardless of the situation with node. import isn’t good even if you only consider the web.
> What module feature could possibly have solved the waterfall problem?
I will give one example of an alternative approach that could have been taken: start with the expression form of import(), ship it alongside top-level await, and hold off on the statement form until after we could see how this was used (you could even restrict it to only taking string literals if you want to begin with, doesn’t make a super difference for this argument, but I can see arguments for that). Here are the benefits:
1. You get everything you get with normal import, you just type await import() and use normal declarations with destructuring. There’s no ergonomic difference except for a couple extra characters, and it is less syntax to learn since you don’t need to learn the almost, but not quite, identical importing declaration destructuring.
2. There’s no weird bifurcation of “load semantics” left as an exercise to the implementer, it’s well defined under Promise semantics and gives us time to determine if we need something fancier.
3. This would have punted a lot of meta issues until later, including “what happens if a module throws an error during load?” And “what happens with recursive imports?” All of these questions are less critical in the expression form where the user has recourse (they can wrap it in try/catch! There can be a user accessible cache, etc.) However, with a top level black box statement these become must fix blockers because you can’t just say “oh the user has many good options”, you have to determine some one-size-fits-all complicated behavior.
4. The fundamental asynchronous nature of import() is not hidden from you in a way that makes you feel like you’re doing something fast, and is the actual issue I have with “the bundling debate”. The await makes it clear that this is a “blocking” (to code after it) asynchronous operation that you probably shouldn’t ship in production, as opposed to the situation now where not only do people not understand this, but we continue to add more features that perpetuate this myth (like module preload link tags that are still slower than bundling but probably require you to use a build tool so what’s the point?).
5. We would have had REPL support on day 1! Both in the browser console and in node. This would have been so helpful for debugging.
All in all, this would have been a great incremental approach that solved the immediate problem of needing something better than evaling the text result of an XMLHttpRequest. It gives you all the functionality of the import statement without the facade of a synchronous feature. It would have prioritized top-level await, made features we’re thinking about now much easier to introduce too (like inline modules), and would have almost certainly already been incorporated in node since there would have been no years of going back and forth since you can’t tell what’s a module without parsing the whole file first problem which is what lead to the whole .mjs mess.
This probably would have had to wait until after ES6 shipped since it fundamentally relies on async/await, but that’s actually a huge part of the point: we wouldn’t have shipped a fundamentally async feature prior to getting our async story straight in the language. A post-async JS mindset would have lead to many different decisions. Despite this delay though, there’s a good chance it would have been fully adopted everywhere much sooner, and would have been much easier to transpile, since it’s “just a function” with syntax restrictions.
Just because something took a long time doesn’t mean it wasn’t rushed.