Speeding up the JavaScript ecosystem – The barrel file debacle
marvinh.dev
marvinh.dev
if (process.env.NODE_ENV === "development") {
module.exports = require("./another-file.dev.js")
} else {
module.exports = require("./another-file.js")
}
Into the destination path, removing this module entirelyI tried to make this work for ESM too, but there were too many edgecases due to module namespace objects being observable (`import * as `)
A similar thing could be done for barrel files with `"sideEffects": false`. We don't currently follow nested default exports which makes this harder to do as effectively (we do support "sideEffects": false)
Instead of detailed analysis of the barrel files in a specific project+dependencies, there are vague exaggerated comparisons like "What's quicker? Having to load 30k files or just 10? Probably only loading 10 files is faster." Of course 10 modules is faster. But are there 29,990 barrel modules in Webpack?
Also, why is running esbuild+node faster than just node? Does that suggest the problem is not just lots of dusk reads, but something inefficient in the way node processes the module graph? Could that be improved?
Anyway this is an interesting direction of inquiry, and definitely worth considering!
That's fair criticism. Whilst the setup shown in the article is synthetic, the experience is from working on real world projects. I've worked on a bunch of projects which had about 3000-6000 barrel files easily, and these then lead to every file in the project always being included. The total amount of files being loaded was somewhat around 30-60k, depending on the project.
I did the exact same thing as described in the article and wrote a custom test runner for jest that bundled the test files with esbuild before executing them. Even though bundling introduces a costly overhead on its own, it was still about 60% faster to bundle + run the bundled code vs not doing it. Bundling can also be amortized because it only needs to be done once per test run vs once per file. That's where the real gains came from, because in jest every single test file constructs the whole module graph from scratch.
I haven't checked myself, but there is likely improvements to be made in node (or other runtimes) to address this. Hopefully, some folks get inspired by this article to look at that.
• 0.15s ÷ 500 = 0.3ms
• 0.31s ÷ 1000 = 0.31ms
• 3.12s ÷ 10000 = 0.312ms
• 16.81s ÷ 25000 = 0.6724ms
• 48.44s ÷ 50000 = 0.9688ms
My own observation on a Surface Book six years ago was that in Node.js under Windows, each module had about 1ms of overhead when there was warm file system cache—that is, simply bundling with Rollup saved 1ms per file. If this sort of thing interests you, quite a lot of useful stuff came out of https://github.com/stylelint/stylelint/issues/2454 which I filed because I was unhappy with stylelint taking over a second to import. And that must have been only in the order of one or two thousand modules, when the behaviour is still close enough to linear.
However when exporting a public/published module it can become onerous to ask the user to import 7 files instead of 7 items from one module.
In a perfect world, this would not be much of an issue because tools would cache these resolutions/imports and their treeshaken code. In the current world, you could prepackage your project even on Node, so that there’s only one file to load, and no extra unused code.
Plenty of large single units in almost any language are _developed_ this way, but is there any particular reason they are _deployed_ that way? It really seems like a distribution packaging issue.
Nothing stops you from prepacking your files, but a lot of node developers are ironically allergic to build tools.
However, the ecosystem tooling is a total piece of crap.
Just the other day I was trying to update eslint dependencies in monorepo only to have builds broken, tailwind stop working on next.js project. Then little tailwind prettier plugin fell off. Some things support ESM, others not yet.
I seriously hope that something like bun will become a one single tool to use. One tool that works with TS, supports linting, testing, tailwind, etc, etc, etc. One single fucking thing that works.
I'd rather use someone elses opinionated approach and never look at the configs again than waste 3 days trying to configure a single fucking eslint config for monorepo.
Now the barrel fucking files is the problem. What happened to cache? Like it's 2023. Can't we just load everything from cache?
Perhaps that's the problem. Tons of moving parts => more chances for something to go wrong.
> I seriously hope that something like bun will become a one single tool to use. One tool that works with TS, supports linting, testing, tailwind, etc, etc, etc. One single fucking thing that works.
I wonder why Node doesn't do that. They have an advantage point of being THE platform for JS development: `node lint`, `node build`, `node foo`, etc.
Arguably this was a good decision in ~2010, but today it makes much less sense, tools haven't been evolving as fast anymore in the JS ecosystem and people seem to be standardising more and more on what works and what doesn't.
There are too many changes and too many moving parts.
This reminds me of an old sketch. Don't remember the details but it's something like:
> Oh, there are 6 popular messenger apps, let us build one to rule them all > some time later... > now there are 7 popular messenger apps
Sourcemaps help somewhat, but even with Deno, when using an IDE, the source you want is often in another castle. You might get minimized code or type definitions.
Contrast with the Go system which rarely has this problem. Following imports goes to source.
import Button from "@cool-lib/core/button";
import Link from "@cool-lib/core/link";
import Widget from "@cool-lib/core/widget";
This is similar to what lodash does too. Still have more imports, but only one package.json dependency! And the same perf gains.
Direct paths also make file structure a part of api and that may not be the desired effect. In addition the bigger problem is not barrel files in your project but rather those in all third-party dependencies
Is that confirmed?
I've been following this issue:
https://github.com/jestjs/jest/issues/6957
And what Jest actually does is still kind of muddy.
In contrast to that, other test runners like AVA have a clear description what happens when:
https://github.com/avajs/ava/blob/main/docs/01-writing-tests...
https://github.com/jestjs/jest/blob/00ef0ed0a03764f24ff568bc...
I see it as a major design flaw, because I have had mysterious, impossible to debug errors happen due to this in just about every project that used Jest that I've been in.
How do you run your project where this happens? I thought everyone complied it separately and then ran it, not both at the same time. Shouldn't your tooling tell you immediately that it's the compilation being slow and not the code?
Compilation isn't the problem, loading and instantiating the module graph is. The timings for that are currently not exposed in any runtime afaik.
Personally I think the readability of having few import lines from shared barrels, compared to a forest of lines, are very useful.
> The term "barrel" is a metaphor. Just like a physical barrel holds multiple items in one container, a barrel file holds multiple exports in a single file, making it easier to manage and import.
https://chat.openai.com/share/93cb04ca-fd6f-46f0-991f-2d7f4c...
In this case the term looked up is literally explained in the linked article. Anyone who read the article shouldn't even be Googling for basic information, unless they need to know specifics that are out of scope for the article's subject.
I opened by asking ChatGPT what barrel files actually are both to verify that the term used in the article is correct and to ensure that ChatGPT and I were both properly referring to the same thing.
Having taken the time to learn this, I shared it with the community. Not sure what I did wrong there to be downvoted and accused of being a "karma farming" bot.
You are suggesting that a ChatGPT output can serve as a reference of some sort. That is not correct. Mentally prefix any ChatGPT response with "A plausible sounding answer to that question is..."
Given that, what is the purpose of your post? Are you asking if what the LLM says is indeed the correct origin of the "barrel" term? Or did you do the research and discover that it is the right meaning, but couldn't come up with the right wording to express that and so relied on ChatGPT, yet you still wanted to be transparent about using it?
Posting ChatGPT responses is fine imho, but they don't mean anything by themselves. For a joke or something, they wouldn't need to. But if you're using it to convey information or ask a question, you have to say so along with the plausible goop it spat out.