9 ms·
ES modules are terrible
- terracottage 5y agoThe worst part is default imports and the linting nazis who want you to use them. 1 file per thing is midwit code organization strategy for people with no actual sense for it.
- xg15 5y ago> And then people go "well you can statically analyze it better!", apparently not realizing that ESM doesn't actually change any of the JS semantics other than the import/export syntax, and that the import/export statements are equally analyzable as top-level require/module.exports. ... "But in CommonJS you can use those elsewhere too, and that breaks static analyzers!", I hear you say. Well, yes, absolutely. But that is inherent in dynamic imports, which by the way, ESM also supports with its dynamic import() syntax. So it doesn't solve that either! Any static analyzer still needs to deal with the case of dynamic imports somehow - it's just rearranging deck chairs on the Titanic. I think while OP's right in theory, there is still a lot of difference between the two: ESM has dedicated syntax for static loading of modules and that syntax is strongly communicated to be the standard solution to use if you want to load a module. Yes, dynamic imports exist but they are sort of an exotic feature that you would only use in special situations. In contrast, CommonJS imports are dynamic by default and only happen to be statically analysable if you remember to write all your imports at the beginning of the module. That's a convention that's enforced through nothing and is not part of the language or even of CommonJS. As an exercise, try to write a static analyser that simply ignores dynamic imports and just outputs a dependency graph of your static imports - and compare how well this works with CommonJS vs ESM.
- jokethrowaway 5y agoYou can detect from the AST whether a require can be statically resolved or depends on non static variables. It may not be as simple, but I venture it's easier to implement than having all the software ever written needing to be migrated
- xg15 5y agoYou can detect simple cases like require("some string literal") on top-level. Things become harder if you have require() in init functions or wrapped in (function(){})() or if the module name is defined in a string constant elsewhere. I can't say how common those forms are, but my point is that there is nothing immediately discouraging a programmer from using them as all of it is "just javascript". So even if you just restrict yourself to static imports, its hard to be sure you caught all of them without running the program.
- wruza 5y agoWrong, things do not become harder when require() is nested somewhere. It may not get called in node, but any bundler looks at it as required-to-be-bundled-anyway. The only case when it’s hard is when require() accepts a non-literal, and that’s symmetric to import(). No extra cases. The programmer is discouraged to use dynamic requires/imports by the common sense, because that makes their app/lib incompatible with most of the cross-end usage. But then we have server-side only packages like pg or express where it doesn’t matter, because browsers provide no runtime (tcp/listen) for them to function and will never do.
- crooked-v 5y ago> that you would only use in special situations There are web frameworks pushing this pretty hard for basic stuff (for example: loading React components with dynamic import) to build page-content-streaming functionality around it.
- wruza 5y agoHow browserify, webpack transformers and others are able to parse require()-s in the middle of a source file, but static analysers are not? These subtly erroneous arguments are the essence of this push. Look, we are maintaining X, Y and Z, and they’re unable to do that R, so it’s bad. No, it’s you making them unable to do that consciously.
- nosianu 5y ago1) importing module "require()" can take a non-static string. A variable or a string calculated at runtime. You can only statically check if that particular feature is not used, but there is no checking the entirety of what require() can be used for/with. require() is more like dynamic imports in Es modules that are awaited and not like the static ES modules. 2) exporting module The other issue is on the exporting module's side: You can do strange things with the "exports" object. ES module exporting is more strict to make it guaranteed statically analyzable.
- wruza 5y agoEs modules that are awaited and not like the static ES modules. But this is a non-argument. Awaited or not, you can’t statically check them either. Developers aren’t idiots and they can read “if you pass a string to require/import, tooling can’t figure that out” in a manual. You can statically check “require(literal)” and cannot “require(variable)”. You can statically check “import from literal” and cannot “import(variable)”. And awaited imports are essentially just memoize(name => (fs.readFile || fetch)(name).then(wrapAndEval)))(name). It’s not a black magic that is available only to “imports”. do strange things with the "exports" object It’s just a value. Somehow analyzers/checkers can work with “var x = do_strange_things()”, but can’t with exports. How is a module boundary different from any other expression boundary? I really don’t want to think that this is pure zealous smoke blowing, but these arguments are as weak as nil, and leave no options.
- forty 5y agoUnless I'm missing something, the exercise is pretty simple, and could be done with grep in a sane codebase (find all require without indentation and with a single string literal inside the parenthesis)
- joepie91_ 5y agoIt's a bit more complicated than that because you'd not want to match such things if eg. they exist inside of another string literal; but the actual implementation of 'dependency collection' in various bundlers isn't too far off from this. They just often do an AST match instead of a string match.
- agumonkey 5y agonow I see files as syntactic module level lambdas where args comes first and the body is the rest :)
- tolmasky 5y agoThis isn’t a real concern, and yes I’ve written a static analyzer for requires (as have many build tools). The fact of the matter is no one is trying to trick the analyzer by passing variables to require, or even more mischievously trying to rename require or something (there aren’t a lot of “(a => a)(require)(path + “/x.js”)” out there). In practice, it is used like a static feature, and when it isn’t, it’s for a good reason that import doesn’t solve and just expects you find a harder solution to. For example, if you want to load a platform specific file depending on your host environment. With import, the entire function now has to become needlessly async just because any use of the import expression needs to be async. Another good example is modules that put their requires inside the calling function to avoid needlessly increasing the startup time of an app for a feature it may not use. This way, only if you call that specific function will you have to suffer the require/parse/runtime hit for it. Notice all these cases are in node, so they wouldn’t result in some complicated decision as to whether to include these “dynamic requires” into the main bundle or not — it just doesn’t come up in bundling since they are use cases that are specific to node. But because of ESM, node now needs to make a bunch of synchronous functions be asynchronous to accommodate a set of restrictions designed with the browser in mind. And again, at the end of the day import does still have an expression form so you haven’t actually resolved the static analysis problem, just made dynamic imports more annoying in non-browser contexts.
- spankalee 5y ago> to accommodate a set of restrictions designed with the browser in mind That's the whole point, and a very good thing.
- tolmasky 5y agoThat was not the whole point, the whole point was to have a feature that could work in a variety of different environments. That’s why this language feature is in the ECMAScript spec and not in the W3C or whatwg, unlike something like “fetch” which is defined by the whatwg and thus has every right to not take other environments into consideration. There is a tremendous amount of subtlety that results from this fact, like how the spec can thus only define a small portion of this feature (syntax and basic semantics), but ultimately needing to leave everything related to fetching, resolving, and executing the code up to the environment (in HTML’s case, the whatwg HTML spec). This really complicates things and creates an unfortunate mismatch in expectations, where most users who have only a passing understanding of this feature and have been sold on the promise of something that will finally “just work” everywhere discover that this isn’t the case at all. There’s a reason why despite being introduced in ECMAScript over 5 years ago it still barely has support in node (and not great support in browsers either btw, but certainly better)— it’s because the reality is that this feature is supremely complicated to implement (despite providing very little tangible benefit), especially in the context of the unrealistic expectations users have developed for it as it continues to be pitched as being the magic tool that will make your code work everywhere without a build tool.
- tentacleuno 5y ago> Yes, dynamic imports exist but they are sort of an exotic feature that you would only use in special situations. I wouldn't necessarily describe dynamic imports as an exotic feature. They are basically required if you're building a semi-large app (and heavily advised by most frameworks in that case!). Otherwise, your homepage is going to load in some complicated AuthorPage / HeavyComponentWithLotsOfDependencies component the user might not even want. The main advantage of ESM in this context would be its asynchrony, which you won't get with require. Webpack tried to tack this on with require.ensure, but it was nonstandard and eventually deprecated. Another advantage of ESM in general would be that you wouldn't have to use preprocessors and compilers like Webpack. My main reasoning here is that, prior to the import spec, web didn't have any form of imports aside from loading in scripts and polluting the global namespace (which isn't necessarily bad). The majority of websites IME use some sort of bundler though, so this isn't really a major change so much as a nice to have, I suppose?
- kabes 5y agoIt's not the fact that imports are top level that makes it statically analyzable. It's the fact that the imports can't be variables like commonjs allows.
- andrew_ 5y agoSay what now? const batman = await import('batcave' + version);
- kabes 5y agoThat's only valid for dynamic imports. But if we consider dynamic imports half of the rant in the post is wrong, since these can appear nested.
- tolmasky 5y agoBut they can. I can construct any hard to find use case just as easily with import. Your analyzer probably won't find this: "(a => a)(path => import(path))(ROOT_PATH + "/x.js")". "But no one would do that!" Yeah, and no one does it with require either. The times people pass in variables to require are well warranted and usually not in a browser context (listed this in my other post, but for example loading "x-mac.js" vs. "x-windows.js" in node, where there is no problem since you're not bundling). So neither in the practical sense nor the hypthetical sense has the "static analysis problem" been solved any more than it was already solved with require. People used require basically like a static keyword before and you could basically make the same usage assumptions as import in those cases. Similarly, you can get as dynamic as require with import, so in the purely theoretical sense not much has been made better either.
- brundolf 5y ago> Yes, dynamic imports exist but they are sort of an exotic feature that you would only use in special situations. They're also decidedly inconvenient to use in most situations, because they return a Promise (unlike require()), which means anything that depends on them has to itself be deferred. This further discourages using them unless you really need to.
- jokethrowaway 5y agoES Modules was what turned node.js into legacy for me, same as python 2/3. Plus transpilers are so slow, it's embarrassing (albeit things are improving with tools written in rust). As someone who's been doing frontend for 20 years and node.js for 10 years, JS development has never been so crap like now. After attending a conference talk about how the TC39 works, I understand why that's the case. TC39 is basically a bunch of engineers from big tech companies who can afford to waste productivity to follow the whims of whatever the group decide. It's completely detached from reality. They operate on a full consensus basis, which means everyone needs to be onboard with the decisions - and if you want your changes to be approved in the future, you'd better play nice with the current change as well. To be honest, I can't wait until browsers get replaced with some native crossplatform toolkit or frameworks in other languages become popular so that we can finally leave JS alone.
- paufernandez 5y ago> To be honest, I can't wait until browsers get replaced with some native crossplatform toolkit That sounds like Flutter to me.
- deleted 5y ago[deleted]
- axismundi 5y agoDon't bundle. The only reason for bundling is too many requests to the server. Use HTTP/2 instead.
- crooked-v 5y agoIf you do that you're stuck with round trip times all the way down the dependency tree. HTTP/2 reduces the overhead of that, but doesn't eliminate it, so now you've added a bunch of loading time to your site.
- axismundi 5y agoI thought that as well, until I read this: https://www.sitepoint.com/file-bundling-and-http2/ https://www.sitepoint.com/file-bundling-and-http2/ https://medium.com/@asyncmax/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2-437c37efe3ff https://medium.com/@asyncmax/the-right-way-to-bundle-your-as...
- crooked-v 5y agoI don't see a counterargument here. "It's fast" doesn't change the fact that using 'native' imports instead of bundles means you're still adding the round trip time for the browser to request each set of dependencies all the way down the dependency tree.
- draw_down 5y agoMadness.
- RedShift1 5y agoWorks... as long as you are next to the server...
- somehnacct3757 5y agoI keep thinking how QUIC / HTTP/3 would go nicely with ESM in the browser (via script tags with type=module) for simple sites. A webmaster could totally avoid the complexity of learning a JS tool chain. Right now even 1000 lines of JS has you reaching for a bundler. It would make shipping a small html+css+js site again as simple as dragging files to your webserver.
- e1g 5y agoBundling will not go away as it solves a different problem of how to best distribute the app to final users. When authoring code, you want to have many small files so you can keep related logic blocks isolated from the rest. When distributing the code, you want to ship a few larger files to reduce network overheads. Any non-trivial frontend app will call code from 100+ files, and your browser is tuned to request these files ~serially (or in serial batches of 8-10) which becomes frustrating very quickly even on localhost.
- joepie91_ 5y agoWorse, even if they were all fetched in parallel, you would still see terrible loading times simply because a dependency graph can only be traversed depth-wise serially. Doesn't matter what network protocol you use or how parallel it is.
- spankalee 5y agoThis is not true at all. For every module you can parse out and load the module's imports in parallel. As you traverse the graph the known and loadable module frontier can grow much wider. The only way it would be serial is if every module only imported one other module.
- joepie91_ 5y agoNote how I was specifically talking about depth-wise.
- cryptica 5y agoIt just sucks that development community decided to double down on bulky build tools instead of trying to optimize server environments to leverage advances like HTTP2 server push to optimistically serve dependencies without latency. It's particularly strange when you consider how popular Node.js is as a server-side environment and how easy it would be to accomplish this since the server is able to interpret JavaScript natively to quickly figure out the client-side dependency tree. My inner conspiracy theorist suspects that maybe the powers that be don't want to allow plain JavaScript to extend its primacy over the web. The way things went makes no sense. Computing dependency trees on the server-side and using it to optimistically push scripts to the browser would have been be far simpler and less hacky than computing source maps, for example. Optimistically pushing server-side resources was supposed to be the whole point of HTTP2...
- austincheney 5y agoThe reasoning presented is only valid if you are stuck holding a bunch of dependencies making use of old conventions. At that moment the complaints about the module approach become a very real concern. That said the problem isn’t modules are all. It’s reliance on a forest of legacy nonsense. If you need a million NPM modules to write 9 lines of left pad these concerns are extremely important. If, on the other hand, your dependencies comprise a few TypeScript types there is nothing to worry about. So it’s a legacy death spiral in the browser. Many developers need a bunch of legacy tools to compile things and bundles and build tools and all kinds of other extraneous bullshit. Part of that need is to compensate for tooling to handle modules that predates and is not compatible with the standard, which then reenforces not using the module standard. When you get rid of that garbage it’s great. ES modules are fully supported in Node and the browser.
- fullstackchris 5y agoAnd yet, the reality IS that 90% of the web is using legacy stuff - heck, even something like 50% of the web still has jQuery on it. (haven't checked the figure in a while, but I guess it is still close to that figure). I think the true anger is that something so essential and basic to JS development has this giant breaking change if you want to switch over to ESM - there's no reverse compatibility or fallback - it just breaks.
- austincheney 5y agoThe solution is some soul searching. Do you really need Babel and Webpack to build a web app? The answer is of course an astounding YES! Most developers cannot add text to a page without JSX, which therefore means React and everything it requires. So when you dig even deeper this is really a people and training problem.
- arh68 5y agoAgreed. So much breakage for so little. If I were teaching JS today I don't know if ESM is worth covering while CJS works at least as well. Maybe next year.
- forty 5y agoI have been doing nodejs backend development for the past 10 years and I have no idea why ES module are needed and who is using them. I assume this is a front end thing. We are using typescript which is using "import" syntax, but as far as I know, it's still transpiling to good old "require".
- throw_m239339 5y ago> I have been doing nodejs backend development for the past 10 years and I have no idea why ES module are needed and who is using them ES modules are part of the Ecmascript spec. DENO uses them. Node.js modules aren't part of the ES spec.
- forty 5y agoI see, so a front end thing indeed :) (and yes deno, which I feel the main purpose is to bring the problems and debates of the front end devs, in an otherwise much saner js backend end world ^^ )
- deleted 5y ago[deleted]
- andrew_ 5y agoYou can tell TS to output ESM. Try using top-level await with TS and you'll run into that. The configuration options are vast.
- tolmasky 5y agoThe “it works without a tool chain” is in fact a ridiculous impractical hypothetical that no one should actually attempt, and yet it continues to make this spec more complicated and unwieldy. For example, to address the obvious performance problem of dealing with loading dependencies, the “<link del=modulepreload>” tag was added, which you’re supposed to include for each individual dependency in your html to let it know to start fetching it ahead of time. So we’ve literally gone full circle and arrived right back to where we started with a script tag for every JS file being replaced with a link tag for every JS file. “But you don’t have to manually do that! Your build tools can just insert the 100 link tags in your HTML file!” I thought all this was to avoid a JS tool chain! If I’m running a build tool I’ll just have it generate one concatenated and minified artifact that performs way better, not this mess! Here’s the documentation if you’re interested in this hilarious feature: https://developers.google.com/web/updates/2017/12/modulepreload#but_what_about_modules_dependencies https://developers.google.com/web/updates/2017/12/moduleprel... Not to mention the security aspects: there is no subresource integrity for imports, so it’s less secure than bundling or using a script tag with CDNs. The point about it being a new syntax is also very valid. Everything import patterns do is almost identical to destructuring, so we should have just extended that feature instead, especially because I do wish destructuring could do those things. For example, if destructuring had an “everything” pattern to complement the “rest” pattern: const { x, …rest, *original } = something(); Where “original” now just contains a reference to the actual returned object, instead of having to break that pattern up into two declarations since the moment destructuring takes place the original object becomes inaccessible. This would have of course given us the “import * as” ability, but is again a feature I regularly find myself wanting everywhere. Not to mention this makes writing code transformations even harder as JavaScript's huge syntax keeps growing and requiring tons of special cases for almost identical statements. The semantics of imports are also very confusing to beginners, as they implement yet another unique form of hoisting. It is so weird that despite being allowed anywhere in your code, they run first. Notice I didn’t say they fetch first, they run first. So for example, the following code is broken: process.env.S3_LOCATION = “https://…”; // The below library expects this as an environment variable. import download from “s3-download”; Oops! Your env variable gets set after every line of code in the import chain of s3-download runs! So bizarrely, the solution is to put the first line in its own import, and now it will run first import unused from “./set-env-variable.js” import download from “s3-download” If the rule is that imports must run before any code in the file, then why not restrict the statement to only being at the top of the file? What is the purpose of allowing you to put all your imports at the bottom? Just to make JavaScript even more confusing to people? Imagine if “use strict” could appear anywhere in the file, even 1000 lines in, but then still affected the whole file. It was already the case that people found function hoisting, "var undefined" hoisting, and the temporal dead zone of let/const (3 different kinds of subtly different hoists) to be confusing in a language that prides itself for being able to be read "top to bottom", why add a fourth form of hoisting? Anyways, the list of problems actually continues, but there is widespread acceptance that this feature would not have been accepted in its current form if introduced today. But for some reason everyone just takes a “but it’s what we got” position and then continues piling more junk on top of it making it even worse.
- throwaway2077 5y agoquestion: // ... if (condition) { const x = require('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart') // do something with x } // ... assume I don't give a fuck about nerd bullshit and I just want the code to be simple and the program to run fast (which it does when !condition because it doesn't need to load hugeFuckingLibrary), can I replicate this behavior with ESM?
- jitl 5y agoYou can replace that require with `await import(‘giantLibrary’)` but now your function needs to be async, and so do all of its callers. This is needed because it’s unacceptable to block the UI thread synchronously importing code in the browser, but in CLI programs not being able to synchronously require is a bit annoying.
- throwaway2077 5y agothat's a shame, I hoped there was going to be a way to do that and it was simply not implemented in node at the time I was looking into it
- tehbeard 5y agoWell since you asked so "fuckin" nicely /s if( condition ){ import('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart').then( x => { //do something with x }) } > ...nerd bullshit... I hate to break it to you darling, but programming is nerd bullshit. edit: alternate that might too much "nerd bullshit", but uses async/await if the surrounding code is an async function: async function doSomeStuff() { if( condition ){ const x = await import('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart'); //do something with x } }
- throwaway2077 5y agono bro, programming is programming and nerd bullshit is nerd bullshit. the arguments I see in favor of ESM over CJS fall into the latter category, at least on the node side of things.
- junon 5y agoThis is a criticism of the tooling, not of the language feature. This is like saying "binding two pieces of wood together is terrible" and using the fact that screwdrivers are poorly designed as your main argument.
- lampe3 5y agoNot sure it the author tried a new build tool like vite, esbuild and so on. Working on large projects and having everything first loaded and then you can load it in the browser is a waste of time that every web developer has every day. Some real world times FOR DEVELOPMENT: Storybook first load: 90 sec, Storybook after first load changes: 3 sec, Vue App first load: 63 sec, Vue app change after that: 5 sec, Vue App with Vite first load: 1sec, Vue App with Vite after that: the time it takes me to press command+tab to switch to the browser Do we really have people that use unminfied unbundled esm in production? If Yes, please comment why? I would also ask the author what about cyclic dependencies? ES Modules resolve them automatically. Something which in large code bases can happen. Why do we still put it through babel? Because most of us don't have the luxury of not supporting old browser... https://caniuse.com/?search=modules https://caniuse.com/?search=modules Even if the not supported browsers for our company is 1% it is still a big chunk of money in the end. and this example: ``` app.use("/users", require("./routers/users")); ``` Really? this is "good code" having a require in the middle of a file? Also funny: The author is annoyed that rollup did not support tree shaking in commonjs and then complains that people are wasting time on esm. Maybe the rollup team does not want to waste time on commonjs? Also then he points to a package which did not got any update in 3 years and would make the hole process he complains is to complex even more complex by introducing a new dependencies. Sorry but the more I read that thing the more it sounds to me like a Junior Dev that does not want to learn new things and just likes to rant about things.
- joepie91_ 5y agoHi, author here. I'm going to ignore the personal attacks and simply point out that my dev build processes typically have a startup time of under 5 seconds even for large projects, and a rebuild time of under 500ms. This is with Browserify. If you are having very slow build times with your existing toolchain, the problem isn't the bundling, which is an extremely fast operation. It's almost certainly going to be one specific computationally-intensive plugin that you either don't need, or would also need if using ESM.
- lampe3 5y ago"Wie man in den Wald hinein ruft, so schallt es heraus" since your from NL it should be easy to translate. These heavy plugins are usually for old browsers to also run in them. That is the only job ob babel. CJS has some deeper problems. - Check if your fav CJS lib freezes the objects? - ESM is more http friendly (mime type)
- deleted 5y ago[deleted]
- cookiengineer 5y agoThis thread and summary are written by someone who has no clue what they're doing in ECMAScript; and who's probably enjoying the fucked up mess that the babel ecosystem created. I'm not gonna dig into that, because reading any polyfill in babel's ecosystem speaks for themselves on how messy, hacky, and actually not working-as-specified most parts are. Instead I'm gonna try to go back to the topic. I think that in practice these are my pain points in using ESM regularly without any build tool. I'm using ESM modules both in node.js and in the Web Browser via <script type module>: - package.json/exports "hack" works only in node.js and not in the Browser as there's also no equivalent API available. This hack allows to namespace entry points for your library, so that you can use "import foo from 'bar/qux';" without having to use "../../../" fatigued paths everywhere (that also might be different in the Browser compared to the nodejs entry points). - "export * from '...';" is kind of necessary all the time in "index" files, but has a different behaviour than expected because it will import variable names. So export * from something won't work if the same variable name was exported by different files; and the last file usually wins (or it throws a SyntaxError, depending on the runtime). - Something like "import { * as something_else, named as foobar } from 'foo/bar';" would be the killer feature, as it would solve so many quirks of having to rename variables all the time. Default exports and named exports behave very differently in what they assign/"destruct", and this syntax would help fix those redundant imports everywhere. - "export already_imported_variable;" - why the HECK is this not in the specification? Having to declare new variable names for exports makes the creation of "index" files so damn painful. This syntax could fix this.
- Ginden 5y ago> - "export already_imported_variable;" - why the HECK is this not in the specification? Having to declare new variable names for exports makes the creation of "index" files so damn painful. This syntax could fix this. You can do: export {already_imported_variable}
- cookiengineer 5y ago...which is a default export, not a named export, as I already explained. My point was about the lack of exporting named exports without the need to declare variable names. Your solution will work only once in a file, therefore it is useless to batch-export lots of imports for the mentioned use case of an "index" file that exports all your classes and definitions.
- api 5y agoIt’s puzzling to me why there isn’t more effort on developing front end Go or Rust frameworks that compile to JS or WASM. It would be a chance to work in a real language instead of this trash.
- aurelianito 5y agoBoth CommonJS and ES6 modules suck. The way things should have been is requirejs. Modules are defined and loading using an API instead of having reserved words. It's really sad what happened to modules in JavaScript.
- jeabays 5y agoEverything JS is terrible.
- aliswe 5y agoi have sympathy for your projects that will need maintenance. but i am making the observation that you're starting to get old - you're being quite bitter over something which is clearly bigger than all of us. on a personal note, im making a cms with es modules and couldnt be happier.
- Aeolun 5y agoI kind of have to agree with the point about loading ESM in the browser. I tried doing this with one of the new fangled frameworks and seeing my browser work through like 5000ish required files was quite comical.
- wereHamster 5y agoUnprocessed ESM in the browser makes sense for local development. We are collectively wasting millions of CPU hours (and developer time) waiting for our mostly unchanging dependencies to be processed. For production deployment though, I'd still prefer ESM in the browser, but not verbatim as they are coming from npm, but compiled, minified, and bundled in a way that strikes a balance between total number of modules, code duplication inside the modules, long-term cacheability etc.
- javajosh 5y agoThe more people start to internalize the truth that "if you ship it you own it" and stop adding dependencies and start removing them, especially if they come with their own wasteful dependencies, then ESM will make sense for everyone. Until then, you're right, devs have to go through unctuous mitigations.
- crooked-v 5y agoIt would be easier to do that if proposals for things like standard library functionality (not the contents of the standard library itself, just the syntax and technicalities of using it) were to go anywhere in, say, under five years.
- noduerme 5y agoJust as an aside... every web app I write these days starts with an index.html page that has a window["deploy"] bool at the top. If that's false, the first script just requires the unbuilt files. If true, it requires the compiled and minified version. I only rebuild when I'm ready to upload.
- emersion 5y ago
- lucideer 5y agoTL;DR: Breaking backwards compatibility is always painful but there's not one actual criticism of ES Modules as a spec here other than its incompatibility with CommonJS
- Aeolun 5y agoThere is one. Which is that you can’t easily do an inline require any more. The rest of the text can mostly be summarized as https://xkcd.com/927/ https://xkcd.com/927/
- bricss 5y agoinline require is most likely a bad design, imo
- nrabulinski 5y agoI’ve been working with node for roughly 5 years and I’ve never liked the hackieness CJS let people incorporate. People, especially a few years ago, were trying to get clever with the require calls, were fiddling around with require cache and while with ESM we no longer can “easily” do stuff like dynamic reloads, I genuinely feel it’s for the better. I strongly agree with privatenumber’s point that import() syntax is the true first class citizen here.
- tehbeard 5y agoInline require(...), Are we back to bad old days of PHP include '...'; midway through a classes function already?
- nuerow 5y ago> Breaking backwards compatibility is always painful but there's not one actual criticism of ES Modules as a spec here other than its incompatibility with CommonJS This. Also, the bulk of the rant is focused on how the author struggles with configuring his pick of JavaScript bundlers and transpilers, and proceeds to come up with excuses to justify not migrating away from CommonJS . This article was a waste of a perfectly good click.
- 5y ago
- amadeuspagel 5y ago> And then people go "but you can use ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature because loading a full dependency tree over the network would be unreasonably and unavoidably slow - you'd need as many roundtrips as there are levels of depth in your dependency tree - and so you need some kind of build step anyway, eliminating this entire supposed benefit. That's not true with skypack, right?
- javajosh 5y agoIt's important not to ignore the possibility that perhaps front-end dependencies are out-of-hand, and need to be reduced. ESM cannot fix a decade of bad practices enabled by front-end build bundlers. ESM isn't there to be a viable alternative to webpack. It's there to enable a different vision of application deployment where apps are smaller, and javascript gets css's transitive import() sub-resource distribution, avoiding the headache of a linear list of global scripts. I really like ESM because I like where it's trying to steer the community of browser application builders. I think front-end builds are terrible on many levels, not the least of which is the obfuscation of code that undermines one of the best features of the web's software distribution, which is its openness. And another major benefit of webapps is that none of the front-end languages require a build step! This makes iteration very fast; if you can make do without the the safety net of a compiler, you can enjoy the speed of not using the bundler.
- crooked-v 5y agoI think the basic problem with that comparison is that people use bundlers with CSS all the the time for exactly the same basic-physics-of-round-trips reasons. It's common for `import()` in CSS to end up exactly what it usually in frontend JS, a dev organization tool rather than something actually exposed to the browser.
- joepie91_ 5y agoIt's a fundamental technical constraint of any tool-less setup. At some point you need to traverse the dependency tree by parsing modules and following imports, and your choice is to do that either: 1) on the client, across the network, one roundtrip for every level of depth, or 2) in a build environment, directly on the filesystem Option 2 means you need some kind of build tool to make it work, and by that point it doesn't really matter anymore whether the tool just traverses the dependencies and makes a list of filenames, or also concatenates their contents into a bundle. And that is why the fundamental premise of ESM cannot work; there are no technical options besides those two. If you want to avoid network roundtrips, you must have build tooling. No way around it.
- bricss 5y agoFirst you create problem with an article, then you fix it with your own magic tool, bravo! https://www.npmjs.com/package/fix-esm https://www.npmjs.com/package/fix-esm
- joepie91_ 5y agoBelieve me, I would much rather not have had to build that hack. But considering that I want my development tools to actually, y'know, work, what would you expect me to do?
- deleted 5y ago[deleted]
- NicoJuicy 5y agoJoepie91 did some work for me long ago If he says there is a problem, he didn't invent it. He knows his stuff
- Chyzwar 5y agoProblem is bothed node.js implementation that leaves most of existing applications without migration path. Even today it is not possible to create full ESM application front or backend. It is worse than python 2 to 3.
- eyelidlessness 5y agoNode ESM support has gotten a lot better through versions 12-17. The biggest problems for workflows that currently work “well” for CJS are: 1. --experimental-loader is more complex and less stable than --require. But it’s also a lot more robust. 2. There’s no equivalent to the require cache, which makes mocking and long running processes like watch mode challenging. This is partly a benefit, as it discourages cache busting patterns like those used in eg Jest which create awful memory leaks.
- theprotocol 5y agoAbsolutely this. It's not necessarily communicated that well in the article, but this is the main reason people are frustrated.
- throw_m239339 5y agoI would argue that Node way of doing things isn't in the ecmascript spec. The problem isn't ES modules, it's node.js. One could answer "well node.js existed prior es module specification". Irrelevant. DENO doesn't have this problem.
- tbrock 5y agoWe recently went through this hell converting a NodeJS codebase to TypeScript. One reason many people willingly enter this hellscape is because we need ES modules for typescript. I say “need” because Typescript wont ingest types from “required” files, you have to import them as modules. So before we converted a single file to TS we has to audit all commonjs imports and exports to convert them to ES modules. I agree wholeheartedly that the end result was a fools errand. I would have rather spent the time adding support for importing types via a require which for some reason returns any “any” today.
- crooked-v 5y agoI think you've confused Typescript's own import/export system with ESM. It uses the `import` syntax, but it's not ESM internally, it's its own thing designed to ingest and export to multiple module types.
- draw_down 5y agoReally, you think GP just made this problem up?
- theprotocol 5y agoThe incompatibility is indeed exponentiated with TypeScript. There is currently no non-hacky way for using both legacy modules and ES Modules in the same project, and many libraries on NPM have moved to ESM-only. TypeScript's transpilation needs to know what to target as regards modules and JS version, which makes things even crazier than they already are.
- vbg 5y agoRather a negative outlook.
- davnicwil 5y ago> for some completely unclear reason, ESM proponents decided to remove that property. There's just no way anymore to directly combine an import statement with some other JS syntax This is one of those 'worse is better' things in language design, I believe. It guarantees simplicity, traded off against extra verbosity. In fact, when it comes to the common and probably most valuable case of reading and understanding code written by others quickly, it is not even a tradeoff really, as both are good. Whether or not that was one of the driving reasons, it certainly is a benefit in my opinion. The two examples given in the post of an inline require don't demonstrate this well, as they're both really simple. I'd say the benefit isn't to stop things examples like that being written and replace them with two lines of code, which admittedly might sometimes be slightly cumbersome. It's that it stops the long tail of much more complex/unreadable statements being written.
- joepie91_ 5y agoI would have considered this a valid argument if overly-clever use of `require` was actually a problem in JS. But it's not! These 'simple' types of obvious cases are the only types of cases that people actually use this syntax for in practice.
- eyelidlessness 5y agoI have seen horrors of abuse of the CJS require cache, I’m glad to hear you haven’t had to deal with it. For what it’s worth, whether it’s bitten you or not, every single instance of my sillycode[1] is in use in Jest (granted in obviously more useful ways). And it’s an enormous headache to debug when it goes wrong. A trivial example: require a logging library which creates a singleton at module definition time and provides no teardown API (yeah that sounds like a bad design but believe me they exist, are easy to find, and hard to replace on a busy and/or opinionated team). If you have a single suite with 100 tests, Jest will leave 100 instances of that singleton running and consuming memory even while totally idle, completely inaccessible to most any machination you might come up with to try to free them. Which isn’t to say ESM doesn’t have this same problem if you try to bust the import cache with eg query parameters. But at least you’ll probably notice it’s a problem because you’re very probably doing it directly and not with some opaque Babel transform that hijacks the entire module system and any code referencing it. 1: https://news.ycombinator.com/item?id=29140847 https://news.ycombinator.com/item?id=29140847 Edit: forgot which sub thread I was in, added link to my sillycode
- TekMol 5y agoES Modules are great. Building JS applications is so much speedier, leaner and more fun now that they are supported widely. One fallacy the author falls for is that they think one needs a build step "anyway" because otherwise there would be too many requests to the backend. Loading an AirBnB listing causes 250 requests and loads 10MB of data. With a leaner approach, using ES Modules, the same functionality can be done with a fraction of those requests. And then - because not bundled - all the modules that will be used on another page will be cached already. I use ES Modules for all my front end development and I get nothing but praise for how snappy my web applications are compared to the competition.
- forty 5y agoI think the trick is mostly not to have a shitload of dependencies. If you have to load a bunch of huge frameworks, whether it's bundled or you have to download thousands of files one by one, it's going to be slower than not doing it at all :)
- eyelidlessness 5y agoIt depends on your bundler and config. You might have “zero” dependencies, but depending on how the code is split you might end up with thousands of small imports nested quite deeply.
- deleted 5y ago[deleted]
- incrudible 5y ago> Loading an AirBnB listing causes 250 requests and loads 10MB of data. How many of these requests are dependent? Lazily loading hundreds of images doesn't impact page responsiveness, but loading an import of an import of an import before your page does anything is unacceptable. > I use ES Modules for all my front end development and I get nothing but praise for how snappy my web applications are compared to the competition. So you actually ship unbundled ES modules? How much code is that? I dare you to bundle it up (rollup/esbuild) and tell me that doesn't improve load times. Comparing to the average website overloaded with crap is a very low bar.
- rado 5y agoHTTP/2 uses a single connection for all modules, no?
- crooked-v 5y agoIf you use 'native' imports in that way instead of bundles, you're still stuck with artificially delayed loading times because the browser has to parse your code, request the first layer of dependencies, parse that code, request the second layer of dependencies, etc.
- incrudible 5y ago"And then people go "but you can use ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature because loading a full dependency tree over the network would be unreasonably and unavoidably slow - you'd need as many roundtrips as there are levels of depth in your dependency tree - and so you need some kind of build step anyway, eliminating this entire supposed benefit. That build step is ideally performed by something like rollup or esbuild, which means I use import/export anyway. If you still use Babel, I feel bad for you son, I've got 99 problems but Babel ain't one. I don't care if the old stuff is not supported, simply deleting 98% of the code in the JS ecosystem would be a step forward. Perhaps that's a minority view, but none of these arguments fly with me.
- emersion 5y agoI'm using ES modules for a webapp I maintain, and it's just nice to be able to run it without any build step. Just fire off a local static HTTP server and you're good to go. There's an optional production build step which can be used if desirable.
- incrudible 5y agoI would admit that this gets pretty slow even with a modest amount of files. If you use rollup/esbuild, you can have a very fast build step that may amortize over the increased page load times.
- dgb23 5y agoIn my opinion as a working web developer, ES modules are half-backed, deceptively simple, do not solve problems consistently and are not built on hard acquired wisdom from other languages. 1) JavaScript could have simply stolen an already good solution. For example namespaces (ex: Clojure/Script, Typescript, even PHP to some degree) provide a powerful mechanism to modularize names - by disentangling them from loading code. They make it straight forward to avoid collisions and verbose (noisy) names. In Clojure namespaces are first class and meant to be globally unique. This implies long-term robustness. 2) Loading modules dynamically should be the _default_. The whole point of JavaScript is that it is a dynamic language. The caveats, hoops and traps that we have to sidestep to for example run a working, real REPL during development is astounding. If you want to be dynamic, go _all_ the way and understand what that means. Yes, it's a tradeoff to be a dynamic language, but why take the worst of both worlds? 3) Like 'async/await', 'class' and many browser features such as IndexedDB it is neither designed from first principles nor fully based on past wisdom. Many things in the JS world smell of "lowest common denominator". Way too much effort is focused on the convenience side of things and way too little on the leverage side.
- jitl 5y agoThe things I dislike the most in software development is dogma, holy wars, and religious crusades about technology practices. I’m not sure to the extent that this happens in other ecosystems, but it seems to happen quite a bit in JavaScript circles. You can ignore these for the most part if you use boring tools and don’t chase the new frameworks-du-jour, but in the case of ESM versus CommonJS I am starting to feel the fire of this war in my dependency graph. My solution in NodeJS programs for now is to use an `esbuild` -based require hook to transpile all the files we import or require into CommonJS on the fly. We need esbuild anyways to run Typescript code without a build step, and combined with basic mtime based caching, it’s fast enough that you really don’t notice extra build latency especially on a second run — much MUCH faster than a Babel require hook. I plan to tune back into this issue once the average comment is more measured and thoughtful, and the ecosystem tooling for dealing with the migration has evolved more.
- andrew_ 5y agoI really, really loathe how major packages in the ecosystem are "We're ESM now, deal with it, sorry about your luck," and forcing the issue. It's arrogant as hell. A hard fork of Node for ESM would have been a much better path (e.g. Deno) That said, the OP's rant is more emotion than fact. > And then there's Rollup, which apparently requires ESM to be used, at least to get things like treeshaking. Which then makes people believe that treeshaking is not possible with CommonJS modules. Well, it is - Rollup just chose not to support it. Rollup was created specifically for ESM. It's not been thrust onto the ecosystem or into anyone's tool chain. One uses it specifically for ESM, and plugins that bolt on for added functionality if they apply. Trying to hammer a nail with a paintbrush doesn't make the paintbrush a bad thing - you just chose the wrong tool.
- devmunchies 5y ago> loathe how major packages in the ecosystem are "We're ESM now, deal with it, sorry about your luck," and forcing the issue I wasn’t able to use the latest version of node-fetch in a node.js script since it doesn’t support commonjs. The project literally has “node” in the name and doesn’t support default node.js.
- theprotocol 5y agoI just encountered this. FYI You can use v2 which still retains commonjs support.
- andrew_ 5y agoI've ended up using last major versions as well. I plan to move to Deno anyhow, and authors like sindresorhus are at least applying security updates to the major version before the switch.
- spankalee 5y agoWere you not able to convert the script to a module, or dynamically import() node-fetch? Since you're using it for an async operation anyway, dynamic import should have worked quite well.
- spankalee 5y agoThis post is terrible, actually. CommonJS was never going to be natively supported in browsers. The synchronous require semantics are simply incompatible with loading over a network, and the Node team should have known this and apparently (according to members of TC39 at the time) were told their design would not be compatible with future a JS module standard. So the primary thing that JS modules fix is native support, and for that you need either dedicated syntax or an AMD-style dependencies / module body separation. AMD is far too loose (you could run code outside the module body), so dedicated syntax it is. Everything else flows from there. I really hate how people blame the standards instead of the root cause which is Node not having taken the browser's requirements into consideration. Culturally, I think that's mostly fixed now, but it was a big problem early on in Node's evolution.
- alerighi 5y agoYes but who cares of native support in the browser? I mean, most JS stuff nowadays is transpiled, written in TypeScript, or if written in plain JS still transpiled anyway to support older browsers, and bundled in a single optimized file. Loading all the dependencies over the network to me is just inefficient, you will have hundreds of requests instead of a single one, you will load the full source not a minified and optimized one, I just don't see the point.
- spankalee 5y agoNative support matters so that we're not eternally required to use tools for even the simplest of cases. Being able to write two files with one importing the other with no npm or bundler in sight should absolutely be a feature of the native platform. And yes, in production you probably will want to bundle, but you probably also want to minify. Does that imply that we should require a minifier to even run any code at all, even in dev? No, of course not. By adding a standard and native support we allow for sites that work without bundling and bundling that can adhere to the standard and not have to even be configured because the input is standard and the output must preserve those standard semantics. That gives tool independence and simplifies usage of the toolchains, and that's a great goal to shoot for.
- aravindet 5y agoThere is a valid discussion to be had about whether the Node.js ecosystem disruption of moving from CJS to ESM is worth the benefits, but the assertion that it's technically worse isn't accurate. A few things ESM does better in Node.js: 1. Asynchronous dynamic import() vs. blocking require(): allows the program to continue while a module is being dynamically loaded. 2. Circular dependencies: ESM correctly resolves most of them, while CJS does not. [example below] I believe this is possible because ESM top-level imports and exports are resolved before JS execution begins, while require() is resolved when called (while JS is already executing.) 3. Reserved keywords `import` and `export` vs. ordinary identifiers require, exports and module: Allows tooling to be simpler and not have to analyze variable scope and shadowing to identify dependencies. I haven't really encountered #3, but I can say I've benefited from #1 and #2 in real-world Node.js projects using ESM. ---- Circular dependencies example: // a.js const b = require('./b.js'); module.exports = () => b(); // b.js const a = require('./a.js'); module.exports = () => console.log('Works!'); a(); Running this with "node b.js" gives "TypeError: b is not a function" inside a.js, while the equivalent ESM code correctly prints 'Works!'. To solve this in CJS, we have to always use "named exports" (exports.a = ... rather than module.exports = ...) and avoid destructuring in the top-level require (i.e. always do const a = require(...) and call it as a.a() elsewhere)
- eyelidlessness 5y agoHere is why ESM is better for static analysis than CJS: module.exports = { get foo() { const otherModule = require('equally-dynamic-cjs') if (otherModule.enabled) { return any.dynamic.thing.at.all } }, get bar() { this.quux = 'welp new export!' return 666 }, now: 'you see it', } setTimeout(() => { console.log(`now you don’t!`) delete module.exports.now }, Math.random() * 10000) if (Date.now() % 2 === 0) { module.exports = something.else.entirely } You can, of course, achieve this sort of dynamism with default exports. But default exports are only as tree-shakeable as CJS. Named exports are fully static and cannot be added or removed at runtime. Edit: typed on my phone, apologies for any typos or formatting mistakes.
- Lhiw 5y agoWelcome to JavaScript, where everything's made up and the points don't matter.
- eyelidlessness 5y agoThe reason ESM is better than CJS or any other JS module system is because of the export keyword. Any discussion focused on imports is relevant but missing the significance of ESM.