Table of contents
- JavaScript’s dirty little secret
- ES Modules
- Introducing esbuild
- Bundling
- Plugins
- Incremental compilation
- Watch mode
- Serve mode
- Caveats
- Conclusion
By now you may have heard of esbuild -- but what the hell is it?
esbuild is an exciting new technology, I say as someone who has been building on esbuild for months. As Evan W. describes it, the creator of esbuild and co-founder of Figma, esbuild is a linker for the web. But what does that actually mean, and why should you care?
Well, JavaScript has a dirty little secret, and that is that you largely can’t deploy JavaScript to the web without tooling. Why is that?
JavaScript’s dirty little secret
JavaScript is an interpreted language. Unlike systems languages like C, C++, and Go, you don’t ‘compile‘ JavaScript to machine code. Instead, JavaScript is evaluated as plaintext at runtime, which also makes JavaScript unbelievably error-prone.
The benefits of a compiler are generally a) performing static type-checking at build-time and b) building to one binary or one binary per operating system. This is simple and elegant and easy to reason about, although perhaps makes it harder to debug runtime errors.
But JavaScript doesn’t work this way. As most of you know, when you ‘deploy JavaScript’, that generally means linking plaintext JavaScript by way of the <script>
tag. This is the happy path for one-off scripts, but what about for the case when you have many files and or dependencies?
ES Modules
These days you can get away with something called ES Modules, which are a way of letting the browser worry about interlinking JavaScript dependencies. ES Modules simply means JavaScript modules, and a JavaScript module can be thought of as a ‘reusable unit of JavaScript’ that the browser evaluates and dynamically links at runtime. The difference here is that we use the <script type="module">
tag instead of <script>
. This is a step up over <script>
but is far from complete.
The reason why ES Modules, convenient as they may be, are generally still not preferred for production is because they -- by design -- defer resolving JavaScript dependencies to runtime. If we could instead do that work ahead of time, it turns out we get better performance gains and backwards compatibility, but this comes with a complexity cost and a learning curve.
This is why linkers are important. A linker is a class of tooling that ‘links’ your code for you so you can worry about the thing you actually care about. Without a linker, you would need to do all the work of stitching your code together in a way that you and your browser, and your users’ browsers, can understand. But we can do better than that! We can use tools like webpack, Rollup, Parcel, or esbuild to automate resolving dependencies at build-time.
So why emphasize esbuild -- what’s new or different about esbuild?
Introducing esbuild
esbuild is a CLI, NPM package, and Go module that makes bundling JavaScript accessible and fast. It is the brainchild of Evan W. and is implemented in Go and JavaScript / TypeScript. It was first released early 2020, and is now 0.9.x (as of early 2021).
esbuild has exhaustive documentation, a highly discoverable CLI experience, and is extremely fast. But what makes esbuild great is how well it solves for a well-defined, constrained problem space.
So what can you use esbuild for?
You can use esbuild to quickly link your JavaScript (js
, jsx
, ts
, and tsx
) and CSS dependencies as deployable assets for the web. And you can do so with bundling or code-splitting, plugins, and more. And what makes esbuild such a quality-of-life tool for me personally is that it implements incremental compilation, watch mode, and serve mode.
There’s a lot to unpack there. I’ll iterate each point now:
Bundling
You can bundle or code-split your JavaScript and CSS sources.
- Bundling is for when you want to deploy a single
app.js
target. - Code-splitting is for when you want to code-split
app.js
into many targets, such asSidebar.js
,Header.js
, etc. Note that this assumes support for ES Modules.
Plugins
The plugin API allows you to preprocess files as they are linked. This is incredibly useful if you want to convert Markdown to HTML or JSX, Sass to CSS, etc. The plugin API defers these implementation details to you.
See the community repo for plugin ideas.
Incremental compilation
Incremental compilation means if you need to compile the same file repeatedly, say for example, as your sources change, you can do so without incurring a performance penalty. This is because esbuild only performs work on changed sources, rather than bundling or code-splitting from scratch every time.
Watch mode
Watch mode means esbuild can ‘pick up‘ changes to your source code as they occur. This means that you don’t need to worry about file-watchers or libraries like nodemon or chokidar; you can offload this work to esbuild and even implement your own watch handlers so you can observe events, log them, push server-sent events or WebSockets, etc.
Serve mode
Serve mode means you can use esbuild as a web server and implement your own serve handler for incoming requests, again to observe events, log them, etc. esbuild actually serves your bundled or code-split targets from memory, rather than from disk. This makes esbuild an incredibly performant web server because it reduces the amount of total work needed per request.
Caveats
Now, let’s talk about caveats. 😱
esbuild is pre-1.0 software and not yet feature complete.
- My experience has demonstrated that this is not a cause for concern. The creator is incredibly mindful about what needs to change, why, and carefully documents every meaningful change in the changelog, even unreleased changes.
esbuild does not perform static type-checking.
- This is hardly a caveat since esbuild is a bundler, not a compiler in the conventional sense, but you can just as well plug
flow
ortsc
into your build pipeline if static type-checking is important to you. (I don’t do this because I rely on VS Code alone for type-checking.) - It’s worth mentioning that esbuild does log errors, warnings, and hints which can help you catch some errors, but these are generally syntactic errors.
esbuild is largely a one-man show.
- This can be interpreted as either a strength or a weakness, but I see this as a strength because esbuild as a technology is incredibly focused. It doesn’t try to solve every problem -- rather, a well-defined, constrained problem space that most frontend developers have or will experience.
- If you take a look at the issues, you’ll notice almost every issue has been addressed by community members or Evan, the creator. Development is pretty rapid and there’s generally a minor release once or twice a week.
esbuild is somewhat at odds with the Babel ecosystem.
- Babel helped modernize JavaScript by enabling developers to write tomorrow’s code today, and that was necessary when JavaScript was underpowered. But today’s JavaScript is far more powerful and expressive than the yesteryears of JavaScript, so I don’t personally see this as a caveat.
- While you may want or need Babel for the latest and greatest CSS-in-JS library, I believe you can still implement whatever additional tooling you may need as a plugin. And if not, that may be a condition for when to not use esbuild.
esbuild supports many, many options.
- The main APIs are roughly only
transform
,build
, andserve
, but the options these functions support are lengthy. Don’t worry if you don’t understand every option’s use-case; I don’t understand all of them. - Note that esbuild’s CLI logger is extremely helpful. You can get very far with experimentation alone. The logger is very helpful about communicating when an option needs to be enabled and why.
esbuild is not designed for HMR.
- HMR stands for hot module replacement, which means state changes are persisted between browser refreshes. This may be a dealbreaker for you, if you’ve grown to love HMR.
- Personally speaking, I don’t love HMR because it makes reasoning about state more opaque. That being said, implementing fast-refresh with server-sent events or WebSockets is incredibly easy and fun with your own watch mode handler.
esbuild is not necessarily ‘for everyone‘.
- If you’ve read this far, experimenting with esbuild is probably for you. 😉 That being said, even if you’re not interested in esbuild, you’ve probably used it indirectly or will. Why? Because esbuild already powers Snowpack, Vite, SvelteKit, Remix Run, and more. There are hundreds if not thousands of tools by now that have or will implement esbuild as part of their build pipeline. Winter is coming.
Conclusion
esbuild is one of those rare technologies that deserves your time and attention. It is already helping to reimagine our industry and I’m incredibly excited to bet on it. It doesn’t feel like other tooling and is here to stay.
Links:
Top comments (4)
I didn't know esbuild until reading this article. After reading, I immediately proceeded to test it with a real-world production application. This application uses Gulp as its build tool, and Rollup to transpile Typescript. I've simply switched the gulp rollup plugin to the esbuild plugin and my total build time - which includes a lot of stuff like sass and other tools - dropped from 10~12 seconds to 5 seconds. Hot reloading after a change on any Typescript file dropped from 4 seconds to 1.2 seconds and this is due to eslint still being run under node, because transpiling time is now only taking less than half a second. Oh, and most importantly: the application still works with zero code change!
Thanks for this write up, esbuild is awesome! It's blazing fast because under the hood it uses WebAsembly. I've been impressed with its performance.
My pleasure.
esbuild uses Go and concurrent algorithms which result in it being a performant tool. While it does support WASM as a host environment, that is not correlated (AFAIK) to esbuild being fast. It’s fast because it only uses V8 / node for resolving plugins.
It’s much easier to beat V8 / node performance metrics if you are building on Go, etc. and leveraging concurrency. As it turns out, most tools in the node ecosystem are built on top of node and therefore V8 which means performance will be hindered and concurrency will be hard to reason about.
I used ESbuild without knowing it, because it is incorporated in Hugo.