Mako – fast, production-grade web bundler based on Rust
makojs.dev
makojs.dev
While I haven’t tried Mako, it seems to have support for advanced code splitting[2]. No idea how powerful its plugin system is.
I cannot attest to that. We are using Esbuild plus CJS at $DAYJOB no problem. Why would that be an issue?
The only bench I'm aware of was presented in November 2023: https://x.com/boshen_c/status/1719596594985681275?t=x8FaB9Aw..., where esbuild was faster.
It would be much better to have projects that work without bundlers, that can use them as an optimization step.
The only thing I'm still missing is an offline JSPM/esm.sh.
Also why not use this config in production? Http2 should give the same performance for multiple small files than a big bundle and it's much better to cache
CSS has advanced enough that I haven't used Less or Sass in many years. Modules make loading easy. Import maps let you use bare module specifiers (or you could use a simple transform in a dev server). CSS modules let you import CSS into JavaScript.
I never use a bundler during development.
I think you might have meant that the developer should hand over all source material after the agreement has been fulfilled.
While Javascript could potentially be contractually mandated to be written in a way to facilitate production codebase recovery, if you knew enough to ask for that you wouldn't, you'd require them to use your source control and to provide build/deployment scripts instead.
Turbopack is supposed to be coming, as a total rebuild of bundling. Rolldown seems solid, as a Rust roll-up redo.
I personally also think about it as a more-likely-to-make-it-to-production Haskell, with how robust the type system, tooling, and other things are (not to rag on Haskell -- it's a fantastic language and there's lots of overlap in the communities).
One of my favourite features is probably that it understands a tsconfig file: https://rsbuild.dev/config/source/tsconfig-path
Hopefully docusaurus gets on that train soon
Killer feature of vite is to leverage existing plugins system of roll up.
Do you have plans to build a compat layer for existing ecosystem?
Other build tools are doing it. Eg: rspack can use webpack plugins, farm can use vite plugin
As for the language "based on Rust", it's likely bad wording due to them not being native English speakers.
Compare that to the expected experience if it was written in C++ or JavaScript or Python or Java or ... All of those are either likely to be slow or painful to use.
Few people do that though.
[1]: https://www.graalvm.org/latest/reference-manual/native-image...
How small is that compared to the JRE? Also I guess this means the program cannot load arbitrary classes?
All this aside, knowing something is in Rust tells me: - It's fast - It's maintainable (imagine the same project but in C)
I meant that Prisma got so much traction despite not supporting JOINs early on.
And then there's me postponing projects because 80/20 doesn't cut it for me. I need to get each and every feature completed before launching.
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?
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.
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.
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.
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.
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.
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.
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.
At long last
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.
- 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.
What exactly is the point of a bundler in the rapid development cycle? If you want your web app to load up fast, it's better if you only need to redownload the parts that actually changed, so you're better off not bundling them.
Where applicable there simply does not exist a better caching strategy than fingerprint plus Cache-Control: immutable
The 99 other ones are still in the browser cache and don't need to be re-(down)loaded.
If I bundle everything, then I have to scrap and reload everything, which is probably great for the final user, but not the developer actively modifying it.
With a slow bundler, that tradeoff made sense, but with a fast bundler, it is suboptimal.
Also, typically the application is split into multiple smaller bundles, so only a slice of the application is rebundled on change.
For development, you don't need to "bundle" at all but you still need to transpile.
To be clear: I have JS/HTML artifacts in my repo alongside Rust source. I want to bundle them then ship them inside a produced Rust binary, or at least with it. With one build step using Cargo.
- https://github.com/oxc-project/oxc
It's also what Rolldown (https://rolldown.rs/about) is basing their in-development bundler on.
It's very cool though and seems like a lot of effort went into this.
But I was more just asking what's the theoretical limit for this kind of optimization, and at the very least with rust. O(n)?
There's probably trade-offs too. Like do you bother with tree shaking to make your end product smaller, or do you not to make your build performance closer to that optimal read-once write-once lower bound.
Vite also uses esbuild to prebundle dependencies for the dev server, but this is separate from production builds.
For dev, you definitely want subsecond recompiles but prod can take a few minutes.
So tired of this ecosystem and its ceaseless tide of churn and rewrites and hypechasing.
Some competition is a good thing, and it seems to have led to a drive for performance in all these projects which I'm not complaining about, but I wonder if more could be gained by working together. Does every major company or framework need their own bundler?
Thinking about it only recently, Go fits in nicely with fast compile times for ' 'builders' esbuild comes to mind. But Rust.. Crazy
Seems like something to bring to WinterCG ? [1]
Yup. I know a few people who were using React 10 years ago and still use it today. What has changed frequently is the tooling. e.g. Bower going away in favor of NPM; Gulp/Grunt going away in favor of Webpack, which is slowly going away in favor of Vite; CoffeeScript going away in favor of TypeScript; AMD/CJS/UMD going away in favor of ES modules, and so on.
ClojureScript has a great deal of stability in both the language itself and tooling, but nowadays I can't give up the developer experience of TypeScript and Vite. The churn in the tooling of the JS/TS ecosystem is wild, but since about 2021 I have found ESM + TypeScript + Vite to provide fast compile times, fearless refactoring, and a similar level of hot-reloading that I enjoyed in Clojure(Script). Can't say I miss Webpack, though!
Does it still use Google's Closure (they've chosen it just for the name, right?) compiler? Is that still supported by Google?
The reason ClojureScript has not moved away from GCC has to do with the fact it performs optimizations -- like inlining, peephole ops, object pruning, etc. -- that ensure ClojureScript's compiler output becomes relatively fast JavaScript code. The closest alternative to GCC's full-program optimization would be Uglify-JS, but it doesn't perform nearly as much optimizations as GCC does.
For a concrete example, consider the following code. I am intentionally using raw JS values so that the JS output is minimal and can be pasted easily.
(ns cljs.user)
(defn f [x]
(let [foo 42
bar (- foo x)
baz (+ foo bar)]
#js {:bar bar
:baz baz}))
(defn g [x]
(let [result (f x)]
(when (pos? (.-bar result))
(js/console.log "It works"))))
(g 0)
The ClojureScript compiler will compile this code to something like this var cljs = cljs || {};
cljs.user = cljs.user || {};
cljs.user.f = (function cljs$user$f(x){
var foo = (42);
var bar = (foo - x);
var baz = (foo + bar);
return ({"bar": bar, "baz": baz});
});
cljs.user.g = (function cljs$user$g(x){
var result = cljs.user.f.call(null,x);
if((result.bar > (0))){
return console.log("It works");
} else {
return null;
}
});
cljs.user.g.call(null,(0));
Paste this into `npx google-closure-compiler -O ADVANCED` and the output is simply console.log("It works");
On the other hand, `npx uglify-js --compress unsafe` gives us var cljs=cljs||{};cljs.user=cljs.user||{},cljs.user.f=function(x){x=42-x;return{bar:x,baz:42+x}},cljs.user.g=function(x){return 0<cljs.user.f.call(null,x).bar?console.log("It works"):null},cljs.user.g.call(null,0);
This is quite larger, and possibly slower, than the output of GCC.My response can be summarized as follows:
- Google indeed uses and supports the compiler
- Google is moving away from the library that shipped with their compiler
- ClojureScript made a wrong bet on technology
- The design of ClojureScript necessitates the full-program optimization of Google's compiler
Announcing with Rust in the title is not because of a hype train, it's a way to communicate that this bundler is in the new wave of transpilers/bundlers which is much faster than the old one (Webpack, Rollup) which were traditionally written in Javascript and painfully slow on large codebases.
While the JS ecosystem continues to be a huge mess, the solution to the problem is not LESS software development ("Just stop making more bundlers & stop trying to solve the problem - give up!"). Or even worse - solve the problem internally, but don't make me hear about it by open sourcing your code.
The huge amount of churn and development in this space has a good reason... it's a desperate attempt to solve the problems that browsers have created for web developers. Fact is that most business has moved to the web, a huge amount of web development is needed, but vanilla javascript can compounds and compound in complexity in the absence of a UI framework and strict typing. So now you've added transpilation and dependency management into the mix - and the user needs to download it in less than a second when they open a web page. And your code needs to work on at least 3 independent browser engines with varying versions.
SwiftUI devs are not a more advanced breed of developer than web developers. So why don't you see a huge amount SwiftUI churn and framework/compilation hell with native iOS development? The answer should be obvious. These problems are handed down from on high
The browser/internet/javascript ecosystem despite its glaring warts is actually one of the most amazing things humanity has created... a shareable document engine grew into a global distributed computing platform where you can script an application that can run on any device in less than a second with no installation. Not bad.
People often scoff at complexity in frontend projects, but they need to handle various types of accessibility, internationalisation, routing and state including storage of those, due to its popularity it's also very frequently an attack surface. With advent of newer technologies (I don't just mean web Dev ones), that's been put into the browser as well, which compounds complexity even more. There's various authentication and authorisation standards most things need to handle as well (not isolated to JS, but it's also not free of it either). Not to mention the versatility and complexity of DOM and CSS that are some of the the most complex rendering engines with layers of backward compatible standards. Like you mentioned already, these engines are all subtly different. Also you have to handle bizarre OS+browser quirks. And things can move between displays with different DPIs, which can cause changes in antialiasing. There's browser extensions that fuck with your code too. Then there's also the possibility that the whole viewport can change. Networks change. People want things to work online and offline so they don't lose work while on a train... While working in an environment that wasn't explicitly designed to support that.
Christ, I'm exhausted just typing this. Most these people complaining probably barely understand what they're complaining about
The complexity is there because everyone is trying to reinvent everything.
> accessibility, internationalisation, routing and state including storage of those
Do multi-pages apps and most of these are really trivial due to the amount of solutions that exists.
> There's various authentication and authorization standards
That's also more of a server concerns than the browser.
> these engines are all subtly different
It isn't the old IE days (which Chrome is trying to replicate). More often than not, I hear this often when people expect to implement native-like features inside a web app. It's a web browser. The apps I trust, I download them.
> People want things to work online and offline so they don't lose work while on a train
Build a desktop app.
> Most these people complaining probably barely understand what they're complaining about
Because it's like watching Sisyphus pushing the stone up again and again. The same problem is being solved again and again and if you want to use the latest, you have to redo everything.
> The complexity is there because everyone is trying to reinvent everything.
That's not just JS. That's literally everywhere. People reinvent ideas in every codebase I've seen. Sometimes it's a boon, sometimes it's a detriment. But again, not something that's unique to JS.
> Do multi-pages apps and most of these are really trivial due to the amount of solutions that exists.
None of these are trivial, even with existing solutions. They're only trivial for trivial cases. Like, I'm sure we both understand people aren't building to-do demos.
> It isn't the old IE days
Probably happened accidently, but it kinda misconstructs what I'm saying. There are issues between renderin engines and variety in how much/quickly they adopt some features. Hell, you still need code branches just for Safari in some cases becaus of how it handles things like private browsing.
> Build a desktop app.
You're trading one world of complexity for another world of complexity (or I guess we could say it's trading one set of platform quirks for a larger set of platform quirks)
> Because it's like watching Sisyphus pushing the stone up again and again. The same problem is being solved again and again and if you want to use the latest, you have to redo everything.
I understand where you're coming from, but just because Svelte was released it doesn't make React (and spin-offs) or Vue less relevant. You're not force to use them.
Regarding the bundling topic, again you're not forced to awirch to a different bundler if you're happy with your existing one, or the project isn't at a scale where it matters.
I think the pressure is internal, not external.
(Meanwhile, swc parser can't yet pass all tests in test262 according to their website: https://docs.rs/swc_ecma_parser/latest/swc_ecma_parser/ )
The problem is that we have already n-thousands alternatives, so it's a slightly different setup everytime - but generally as long as it's not webpack, it's all good.
Recently someone disabled turbopack on a next.js project because one new dependency wasn't supported and the developers started complaining right away the app was unbearably slow. The team couldn't work on latest for a week, they were just reverting the latest changes breaking turbopack support, working and then pushing.
Esbuild is the only current build-tool that keeps one sane. The serve-mode is excellent and elegant with no brittle constantly breaking hacks like HMR or file watching.
Sadly configuring especially the serve-mode is a bit badly documented, and not usable via CLI flags if one needs plugins.
i noticed, and was very surprised by that. surely esbuild is the "standard" fast bundler these days; everyone knows webpack is slow so doing better, even significantly better, than it isn't a very large claim.
https://wayland.emersion.fr/mako/
It can be hard sometimes to come up with names that aren't already in use. I think as long as it's clear in the description what it is, and the same name isn't shared for two projects that do approximately the same thing, maybe it's not so bad. There could also be an issue where command names might be the same so one would have to be changed. I recall this may have been a small issue when the Go language was new, as there was also a game of go available in some distro repositories. I believe that's generally solved now.
For dev I replaced webpack with "bun vite": it loads scripts ad hoc and is thus super fast at startup, and it still supports hot reloading.
For build I use "bun build". I've created a small script where I don't specify the output folder, but just intercept it, and do some simple search & replace things. It works perfectly, although it's a bit of a hack (simplified code):
const result = await Bun.build({
entrypoints: [srcName],
root: "./",
minify: true,
naming: `${configuratorName}.[hash].[ext]`,
});
for (const r of result.outputs) {
let jsSource = await r.text()
jsSource = jsSource.replaceAll("import.meta.hot","false")
Bun.write(outdir + r.path.substring(1), jsSource);
}
It might not be pretty, but it works super fast, and it only took me a couple of hours to convert the whole thing...Update:
For the record, the real script also uses the HtmlRewriter from cloudflare (included by default in bun) to alter some basic things in HTML templates as well...
[0] https://bun.sh
https://medium.com/chia-ux/what-do-chinese-clients-mean-h5-i...
> HMR
Hot Module Replacement allows all kinds of modules to be updated at runtime without the need for a full refresh. (Abridged from a webpack doc)
I prefer to spend my time building against that instead of another bundler.
The amount of bundlers I've seen in my time is borderline obscene. Nowadays it's even worse, as every javascript framework developer's actual secret fetish is to build their own bundler. Ideally in Rust because that's hip I guess.
Webpack, Snowpack, Parcel, Rollup, Esbuild, Vite, Turbopack... just stop. Enough.
I've managed to cut down build times from ~1min (sometimes up to 3, but I couldn't even tell you why) when using Webpack and Babel to less than 200ms using just Rsbuild.
So, I welcome the improvement! The fact multiple people/orgs felt the need for this clearly means they felt the pain of slow builds too.
I'm sure people tied up in the roll-up ecosystem think the same about Vite and rolldown!
All these do things subtly differently in ways they think is the correct way. Maybe one will come on top or maybe something else comes along and integrates lessons from both and that eventually wins and all meintence moves there eventually.
The JS ecosystem is complex (for better or worse), and bundling for it isn't a simple as people believe. So it makes sense there's multiple things trying to tackle the same problem!
We already had our bundlers in Java and .NET land, before nodejs came to be, and life was good.
People (re)write such tools either for fun or to solve a problem (or best: both). Apparently after so much re-writes the problems haven't been solved. To me, this indicates fundamental problems. I'm not familiar enough with the ecosystem to know what those would be, let alone how to truly solve them.
But the very fact that we see new builders, transpilers, bundlers every few months is enough to conclude we aren't solving the problems on the correct level or that maybe it cannot be solved at all. Because otherwise one of the many attempts would've solved the problem and "everyone" would be using that.
NextJS' documentation is also *terrible*. This situation is made all the worse by any material online about NextJS that's more than 3 months old being totally inapplicable because the framework changes so often.
It is marketed as the solution to slow starts but React is slow so the solution is terrible over-engineered.
A much better fix is to remove React and use something that is already fast like solidjs or Lit. There are much better UI Kits in Lit that I have seen in React and in the end it is just JS so the same people that can could React, can code Lit and SolidJS.
If you are happy with client-side rendering and do not desire React on the server, there is not a strong reason to use Next.js; it introduces complexity and churn.
Vite wrapps esbuild. Not sure what it provides itself.
Then there came several specialized (Rust) tools. For the same reason esbuild was made.
I think ultimately they try to solve the same issue:
JS is supposed to be a productivity gain over compiled languages. But with ultra slow builds that goes out of the window.
But what business problem are we solving? Why do we need compiles, transpiles and so, in a dynamic language in the first place¹? And if so, is compiling the right solution to that problem?
My point was mostly to question if we are solving the right problem. And if the direction in which we are solving it, is the right one. After some 20+ years we still haven't converged around a single solution. But instead we keep firing out "new" solutions and solutions to the problems that those solutions then introduce on an almost weekly basis.
To me that shows we are either simply looking in the wrong place, or have a much deeper, fundamental problem that simply cannot be solved. And should probably either stop looking for the solution or just abandon the whole stack.
¹ I'm not looking for an answer to this question. I know several reasons why we build, compile, transpile, minify and whatnot in JS. But all those are also solutions to deeper problems. Problem's that can be solved in several ways, only one of which is "compile pipelines".
It's a snow ball rolling down an infinitely long mountain. I believe this may never settle.
Mainstream browsers have already coalesced on a no build solution but it's profitable, by fame or fortune, to continue building solutions that require bundle and compile steps. Then others use those because off the shelf libs require them and save time and money.
Also if it is such a great experience, people wouldn't be rewriting the node ecosystem in Dart, Go and Rust.
In many cases you can achieve the same with a clearer separation, with data driven methods and by generally not running so much JS.
The typical example is input validation. You want immediate feedback on the client, but obviously you validate on the server.
But instead of running literally the same specific code twice, you can use json-schema or your own general data description of what valid input is. You move the specifics from code into data.
It's probably fine, but so are all the others as well. The authors have probably spent a fair amount on time on this project so I don't want to be negative but it's just hard to be excited when it brings nothing new to the table.
Why should I use this over Vite or esbuild? Because it's written in Rust? I don't understand why that even matters. Even if it was 10 times faster I wouldn't use it because Vite is fast enough and have all the plugins I would ever need.
Also note that the Vite team is writing Rolldown[3], and guest what? They are writing it in Rust.
[0] https://crates.io/search?q=deno [1] https://crates.io/search?q=oxc [2] https://crates.io/search?q=biome [3] https://rolldown.rs
But the language itself is not a goal or at least shouldn't be IMO. Thus it have the opposite effect on me, who do not care about what language my bundler is written in.
If I did, it still wouldn't have any competitive advantage since as you point out Vite will soon also be based on Rust.
This is very true and almost all of them are taking far longer to develop than they initially thought. swc/turbopack is being pushed by Vercel and it has been a huge ongoing disaster.
I noticed a second or two in lag between launching the terminal and getting a shell prompt. Commenting out the nvm load as a test removed the delay. I installed fnm, aliased it to be nvm, and everything is snappy. Also nicer if you use tooling to 'nvm use' when changing into a project directory.
There are a few issue threads such as this one : https://github.com/nvm-sh/nvm/issues/2724
BTW, this blog post was great for finding the culprit if there is zsh startup latency : https://stevenvanbael.com/profiling-zsh-startup
Their reasons are to have fast builder with flexisbility needed for business cases. If other words they are making internal tooling publically available.
Being faster than es-build is not a goal, get people excited about speed is not a goal. Have control over tooling, flexibility, be fast-enough, be opensources are the goals.