Hacker News new | ask | show | jobs
by tolmasky 1685 days ago
It is absolutely not excellent, unless you restrict yourself solely to whether there is a checkmark next to the browser name in mdn. It is very buggy, very difficult to debug, and as I mentioned in another comment, missing serious features (like no subresource integrity which, means we’re encouraging people to use a much less secure system of importing scripts in many cases!)

> It really is the whole point. TC39 was not going to define a feature that didn't work in browsers, full stop.

No one is saying they shouldn’t have considered the browser! We’re saying they should have also considered other major environments, like node! That’s the way to design a language feature and absolutely what they wanted to do. There were a number of reasons it was rushed out the door, but they’re pretty upfront about the fact that they would do things differently now and basically no other feature would be allowed into the spec in the state ESM made its way in then. I am currently a TC39 delegate and can assure you that it’s OK to admit when things aren’t great so that we can learn from it. It’s how we’ve gotten JS to such a better state than where we were 20 years ago, not by bending over backwards to defend the with() statement.

1 comments

I've been using almost exclusively standard modules for years and they work quite well. Old crashers and cache problems I was aware of have been fixed for many years now.

I don't know what debugging problems there are that wouldn't exist in CJS. At least with native modules you can see individual requests in the network panel while you're working and individual files in the sources panel while debugging. That alone is a huge increase in debuggability to me.

SRI really should be done out of band. Inline SRI would require far to frequent cache invalidation and isn't compatible with package manager workflows where you don't know the exact version of a file you'll depend on. A tool rather should build up an SRI manifest similar to a package lock. This has been discussed several times in module and import map threads.

And I don't think JS modules were actually rushed. They were languishing for years with an overly complex loader API and cut down to an MVP with the core agreed upon semantics and that's what finally got everyone to ship. They're really fine, and with import maps, CSS modules, and eventually web bundles, will be far, far superior to any previous alternative. They already are.

The fact that Node has some JS module / CJS interop issues (and really only when you try to use JS modules from CJS, the other way is fine) is Node's problem, not TC39's. There's really nothing that TC39 could have done here because synchronous require() is the fundamental problem. It was a bad design from the start and we shouldn't burden ourselves with that bad decision forever. The sooner CJS goes away the better.

> SRI really should be done out of band. Inline SRI would require far to frequent cache invalidation and isn't compatible with package manager workflows where you don't know the exact version of a file you'll depend on. A tool rather should build up an SRI manifest similar to a package lock. This has been discussed several times in module and import map threads.

If you are introducing a tool, then you should be bundling your code, not creating more metadata files to load! Bundled code has repeatedly proven to load faster than ESM code using whatever HTTP 3 prefetch mumbo jumbo you throw at it, that no one actually uses in practice anyways. If you have a tool chain, then the answer is easy: don’t delay fetches and create more HTTP requests and roundtrips! As I mentioned in another comment, the sheer ridiculousness of this is demonstrated in the <link rel=“modulepreload”> feature [1], where I kid you not the actual recommendation for having performant dependencies is to litter your HTML file with a link tag for ever JS file that’s imported. We’re right back to where we started with a top level script tag for every script! Argh! But I know, now the recommendation is “oh no silly, your build tool should just create your 100 link tags”. Again, why? If you’re using a build tool a bundled file is way faster than waiting for link tags to get parsed to issue a bunch prefetches and on and on. It’s so frustrating to discuss ESM because the goal posts kerp bouncing between this being an “easy to use feature that removes the need for build tools” only to have every issue with it hand-waived away as trivially solvable by a build tool that ultimately creates a worse end-user load experience than what we already have.

> The fact that Node has some JS module / CJS interop issues (and really only when you try to use JS modules from CJS, the other way is fine) is Node's problem, not TC39's. There's really nothing that TC39 could have done here because synchronous require() is the fundamental problem.

I think part of the disagreement stems from the fact that you believe my position is that we should have “done nothing” or that the only options were “the system we shipped” or “just do it the node way” or something. That’s not the case at all. I am all for a standard system and I recognize the problems with require(). It is simply the case that a design that took into account the requirements of node could very well have served both systems. These aren’t the only two possible require systems imaginable, but an API that exclusively looks at only one set of constraints, despite billing itself as a general purpose solution, will not generate the best end result. This is shown by the several bandaids that had to be added after the fact to import and could have been avoided if considered beforehand.

1. https://developers.google.com/web/updates/2017/12/moduleprel...

Your complaints seem to have far less to do with JS modules and much more to do with the lack of native bundling in the platform.

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.

What module feature could possibly have solved the waterfall problem? The only options are manifests and bundling. IE, you can't magically tell a browser what it should load without telling it what it should load. modulepreload is essentially a manifest and Web Bundles are bundles, so there are solutions covering the space. Browsers should get behind Web Bundles, asap, since that would also solve many problems for caching, CSS and other assets.

The problem I have with this debate is that JS modules get criticized for being able to work without bundling at all, when that's purely a positive and they still can be bundled for performance.

If you don't like the unbundled workflow, don't use it. It's still useful to have a standardized syntax and semantics, and very useful for those of us who do want to use them unbundled: for simple cases, dev environments, or combined with features like prefetch/preload.

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