Hacker News new | ask | show | jobs
by benrutter 720 days ago
I don't work in web, and possibly live under a rock. I'm a little confused around what bundlers actually do?

I'd sort of assumed it was a typescript build thing before, but Mako's page gives me enough info to make me realise I'm wrong, but seems to assume people are working with some base knowledge I don't have.

Any pointers to information of exactly what bundlers do? The emphasis on speed makes it sound like it's doing a whole bunch of stuff, what are the bottlenecks? Package version resolution?

6 comments

Are you familiar with Java?

If so, a web bundler is like a build tool which creates a single fat jar from all your source code and dependencies, so all you have to "deploy" is a single file... except the fat jar is just a (usually minified) js file (and sometimes other resources like a css output file that is the "bundled" version of multiple input CSS files, and other formats that "compile" to CSS, like SCSS [1] which used to be common because CSS lacked lots of features, like variables for example, but today is not as much needed).

Without a bundler, when you write your application in multiple JS files that use npm dependencies (99.9% of web developers), how do you get the HTML to include links to everything? It's a bit tricky to do by hand, so you get a bundler to take one or more "entry points" and then anything that it refers to gets "bundled" together in a single output file that gets minified and "tree-shaken" (dead code elimination, i.e if you don't use some functions of a lib you imported, those functions are removed from the output).

Bundlers also process the JS code to replace stuff like CommonJS module imports/exports with ESM (the now standard module system that browsers support) and may even translate usages of newer features to code that uses old, less convenient APIs (so that your code runs in older browsers). And of course, if you're writing code in Typescript (or another language that compiles down to JS) your bundler may automatically "compile" that to JS as well.

I've been learning a lot about this because I am writing a project that is built on top of esbuild[2], a web bundler written in Go (I believe Vite uses it, and Vite is included in the benchmarks in this post). It's extremely fast, so fast I don't know why bother writing something in Rust to go even faster, I get all my code compiled in a few milliseconds with esbuild!

Hope that helps.

[1] https://sass-lang.com/documentation/syntax/

[2] https://esbuild.github.io/

I already knew what bundlers do, but I’ll just say thank you anyway for writing such an approachable explanation. I might refer to it in the future when someone asks ME what a bundler does :-)
I'll admit to being a little outdated on front-end design evolution. Sass/SCSS is no longer needed? Does CSS support nested blocks now?
Yes it does!

https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_sel...

All major browser do now support it. However I still use the PostCSS Nesting plugin:

https://www.npmjs.com/package/postcss-nesting

This lets you write the syntax from the specification but it will be transformed to CSS that still works in older browsers, kind of like a polyfill in js.

Very recent addition, but yes!

At long last

Thanks! I really appreciate the detailed explanation- makes a whole lot of sense.
Thank you. Extremely helpful.
Bundlers take many - usually at least hundreds, often tens of thousands - individual source files (modules) and combine them into one or few files. During that, they also perform minification, dead code elimination and tree shaking (removal of unused module exports).

It's orthogonal to TypeScript - bundler will invoke a TS compiler during the process and also functions as a dev server, but that's just for nicer DX.

Package version resolution is done by package manager, not bundler.

When you say dead code elimination, do you mean if I import some huge library just to use a single function, the bindler will shimmy things about so only the single function is being included in package and not the big library?

If so, that's amazingly helpful, I'm mostly over in python data land and I wish that existed for applications, although admittedly there's less need.

Yes, exactly. Pulling a huge npm dependency is usually not a problem if they didn't go out of their way to make it super hard to analyze at build time.

This is tree shaking though, dead code elimination means it will find code that isn't used at all and remove it - for example you might have if (DEV) {...}, and DEV is static false at build time, the whole if is removed.

So first it performs dead code elimination, then it removes unused imports, and then it calculates what is actually needed for your imports and removes everything else.

That's very cool! I already knew that this was something compilers did, but somehow never even considered you might do the same for an interpreted language like js.

Makes me wonder why some js bundles are still so big, am I over hyping what dead code elimination and tree shaking might achieve? Do some teams just not use it?

Either way, I've come away from my question with a pretty big reading list. This is exactly what I love about HN.

I think it's not so much about interpreted vs. compiled but more about the delivery of client code to the user - every time any user visits any website the browser may have to download the code (if not cached), then parse it, then execute. The less code that needs to be shipped, the faster time to interactivity and also less bandwidth usage.

Some bundles may still be big if teams don't use it, and some libraries are not structured in a way that facilitates dead code elimination.

Consider libraries that use `class`, such as moment.js, all functionality is made available as methods on the Moment class. If you only use 1 method, you still have to bring in the whole class. Whereas if a library is structured as free functions and you only use one, then only that gets included and the rest is eliminated.

Conditionally yes. There are many libraries that cannot be tree shaken for various reasons. Libraries typically need to stick to a subset of full JS to ensure that the code can be statically analyzed.
Basically the only forbidden thing is dynamically calculating import paths, or dynamically generating the module.exports object.
Yes, here's a few excellent articles that explain what problems build tools solve and why they exist:

- https://sunsetglow.net/posts/frontend-build-systems.html

- https://www.innoq.com/en/articles/2021/12/what-does-a-bundle...

- https://www.swyx.io/jobs-of-js-build-tools

Loosely put, they're the equivalent of all of `gcc` or `rustc`: compile the source code, run type checking, output object files, transform into the final combined executable output format.

Bundling is the equivalent of static linking, typically combined with dead code elimination (which is called "tree shaking" in the web world) plus optionally other optimizations and code transformations.
Dead code elimination is related to but distinct from tree shaking - it also means that unused code branches get removed, for example constants like NODE_ENV get replaced with a static value, and if you have a static condition that always results to true, the else branch is removed.
In my book that's all covered by the term 'dead code elimination', e.g. removing (or not including in the first place) any code that can be statically proven to be unreachable at runtime. Some JS minifiers (like Google's Closure) can do the same thing in Javascript on the AST level (AFAIK Closure essentially breaks the input code down into an AST, then does static control flow analysis on the AST, removes any unreachable parts, and finally compiles the AST back into minified Javascript). Tree-shaking without this static control flow analysis doesn't make much sense IMHO since it wouldn't be able to remove things like a dynamic import inside an if that always resolves to true or false.
Yep, that's how it works - you first perform dead code elimination and then tree shaking exactly because it wouldn't remove everything otherwise. Agreed that you need both done one after another in most cases; however you can usually disable either one in bundler configuration and it's a separate step.
https://makojs.dev/blog/mako-tree-shaking explains how mako do the tree shaking stuff, but in Chinese.

In my two cents, the tree shaking is more focus on removing unused exports in ES module at top level. it's a mixing with Dead code elimination and link time optimization.

If you are also looking for broader context beyond what a bundler is, I have written a broader exposition on frontend builds here, which may be useful in understanding how bundlers compare to adjacent build tools: https://sunsetglow.net/posts/frontend-build-systems.html.
Thanks, that's actually exactly what I was after without realising it!
I've always liked the analogy of a compiler/linker for web assets, personally.