Published on: Tue Nov 09 2021
If there is one take away from this post I wouldn’t mind it being this. I think esbuild, in its current state of development, is great for Typescript projects like utility libraries or monorepos, node applications and lambda functions.
However, I don’t think it is quite ready for large production client applications due to some optimization options being missing which are offered by other bundlers like rollup and webpack (ie dynamic import, lazy loading, prefetching). There are some more things that needs to be ironed out there.
All that being said, it doesn’t mean esbuild lacks features, it just means if you need some the advanced features of a bundler then it may not be ready yet. Please check out my example repo to see some of its current capabilities.
Above all, esbuild is one of those great tools to have in your toolkit. It is lightning fast, lightweight and offers enough features for most bundling needs. You can get a setup to bundle typescript within seconds.
No need to fiddle with huge config files, pull in multiple loaders and plugins to get something working. It really cuts in at the core of complexity by offering enough features for most of the projects out there (especially if you are looking to bundle few typescript files together).
Oh, it supports both a Go(lang) and JS api. Neat huh ? So, if your team uses Go(lang) then you’ll love esbuild.
As developers, we are always looking for better tooling to add to our toolkit. Some may say its for the joy of learning new things but that is only part of it.
The other part is, well, because it’ll make your life easier or better. That is what a good tool is supposed to do anyways.
Today, bundlers are part of every development team’s workflow. If you are shipping website or web applications to the end users, then you are likely using webpack or rollup.
As the bundler ecosystem started to evolved. There seems to be a common thread which is offering better performance. Then, esbuild was introduced, and it completely blows everything out of the waters.
It is not just 10x faster but 100x faster than its runner up (source).
The simplicity of its API reminds me of the unix philosphy. Granted the tool is written in Go, so there might be a bit of a influence there.
If that does not convince you, there are many popular projects and teams adopting the tool, and they include (and are not limited to):
Seeing as more and more popular projects are adopting it, it might be a good idea to dig into the core technology, and evaluate its strengths and drawbacks.
Even if you don’t use it directly, you might start exploring using esbuild through a loader in webpack or rollup. So, you never know when you might have to dig into the library to make some modifications.
Also, the creator, Evan Wallace, is one of the founders of Figma.
I keep wondering how he is time to work on this stuff. Either way, hear it from himself, here is why he is building the tool:
I'm building esbuild because I find it fun to build and because it's the tool I'd want to use. I'm sharing it with the world because there are others that want to use it too, because the feedback makes the tool itself better, and because I think it will inspire the ecosystem to make better tools.
Inspired indeed. I think this tool along with other tools written in rust (ie swc) may just kick off the Renaissance period of web tooling within the ecosystem.
Personally, I find the simplicity, and versatility quite refreshing. It is a lot like a swiss army knife!
So, let’s explore what the job of a bundler is, and then take a look at some of features esbuild offers.
This is not required but here are some of the terminology that would make it easier to understand some of the later sections.
Superset language - This is an extended language built on top of one which is supportly natively by a platform (ie browsers)
Code splitting - This is a reference to bundling together javascript where we split off part of a script to be loaded in later
Tree shaking (or dead code elimination) - This is the eliminiation of un-used code in your bundle when cominbing the script together
Side effect - This is a reference to splitting off or eliminating code where it can create a “side effect” which other scripts are reliant on
Build - Running a build is the process to initialize the bundling process to combine all the assets together (js, css, html tec)
Transform - Can be part of a build but the main duty is to perform a transformation (from language to another. ie Typescript to Javascript)
Plugin - This is a extension of the bundler where you can “hook” into different parts of the bundling process (start, middle or end etc) to add customization
Bundling sounds great but why do I need it ?
Well, first of all, if you use any modern stack (create-react-app
or vue
) you are most likely using webpack or rollup for the bundling your assets.
In the development process, software is design and built in a modular way (files are split up rather than all in one file). Likewise, many superset languages (sass, scss, typescript) and custom javascript syntaxes (decorators, generators etc) are used.
These are to further extend the languages in order to aid colloboration, improve code maintainability and developer experience as the codebase get larger.
One modern example is the javascript superset language, typescript. Typescript helps to enforce a contract in the code by adding static typing to javascript. That way other developers can quickly evaluate and analyze the contract provided by the code written by someone else.
Here is an example (javascript vs typescript):
// javascript
function transformUserReviewData(data) {
// some code
}
// typescript
interface UserReview {
id: string;
name: string;
places: string[];
reviews: {
id: string;
rating: number;
}
}
function transformUserReviewData(data: UserReview) : UserReview {
// some code
}
From the example, it is evident what the contract is in the typescript code. The code is expected to receive UserReview
and it returns the same data back in transformUserReviewData
. If any part of this contract is not enforced in the code, the compiler will complain about this error.
Whereas, in the javascript example, I really can’t know what to expect from this function unless I read and understand the whole function. What is data ? what is the data type being returned ? would I get an error if I use it in the wrong way ?
Of course, similar contracts can be setup through writing testing but typescript just makes it that much easier, especially with built autocompletion and hinting when using with VSCode.
Finally, when it comes to serving the website to end-users, our browsers cannot run typescript or any of these superset langauges used to aid the development process. So, this is where the bundler comes in.
The bundler will package these independent scripts, and assets into an efficient bundle (ie css, js, png, svg) ready to be sent to the end-users when they visit the site.
At a high level, that is one of the main goal of these bundlers like webpack, rollup and others.
Now we have a good understanding of the bundler, let’s dig into the features of esbuild.
Here are some of the features it supports:
process.env.NODE_ENV
)jsxFactory
)esbuild-wasm
you get the same api in the browser)Looks like there are a lot of great things but what’s the catch ? What is the trade-off ?
tsc
)script[type="module"]
)Having used esbuild for some time, I find it very easy to use. I don’t think it completely replaces webpack because the two tools has use cases in different situations even though some of their feature set overlap.
Even though the feature set of the tool is lean, I still think esbuild can still cover 80% of the use cases.
Here are some of the features I find interesting and neat.
The plugins are very easy to build and use. However, it is still experimental.
There are various points in the build that you can hook into, they are the following:
Again, as I mentioned, the tool is quite narrowed scope and in its roadmap. Hence why you see such a condensed API for the plugin which can be a concern if you need more functionality which webpack offers.
The trade off however is the complexity. In my opinion, it is much easier to build a plugin in esbuild than webpack because the API surface area is so much smaller. More feature does not necessarily mean better if you don’t need it all.
Here is an example of a node externals plugin I wrote (ignores node_modules
from the bundle).
To read more plugins refer to the plugin docs.
esbuild offers a define
option which allows you make build time replacement within your code. Other bundlers require an extra plugin to achieve this.
esbuild.build({
define: {
'process.env.NODE_ENV': '"production"'
}
})
Note: You have add in the quotes for string values because define can also replace inline code.
You can also inject polyfills or add utility into the global scope of your codebase. Even make build time replacements of how some parts of your code is used within your codebase.
This can be combined with the define to offer powerful features in adjusting the codebase for different needs and even provide optimizations at the system level (ie between environments).
Example:
replacing sendEvent
with a method that offers batching within the application in production
environments at build time.
// event-batching.js
async function batchSendEvent() {
// ...code
}
// build.js
const productionConfig = {
define: {
'sendEvent': 'batchSendEvent'
},
inject: ['./event-batching.js']
};
esbuild.build({
...(process.env.NODE_ENV === 'production')
? productionConfig
: {}
})
esbuild makes tree shaking quite easy as it is done out of the box. It offers several configuration options to handle various cases.
Tree shaking can sometimes be error prone when dealing side effects in your bundle, so, it is good to understand the various ways the bundler optimizes the final bundle. When you eliminate the wrong code for optimization that can potentially break your whole bundle.
Let’s have a deeper examination into how this is achieved.
esbuild offers a few configurations that are related to tree shaking. They are listed below.
Configuration options:
treeShaking
: boolean - enable tree shaking (default: true)
pure
: boolean - specify a “pure“ or tree shakable function (ie. { pure: ['console.log'] }
)ignoreAnnotations
: boolean - ignore the annotations that hints to the bundler for tree shaking
/* @__PURE__ */
, /* #__PURE__ */
sideEffects
define
: { [key: string]: string } - key and value variable to be replaced at build time for conditional tree shaking
false && function myFunc() {}
When using the inline syntax, esbuild makes some decisions for automatic tree shaking depending on how your code is used.
Those are the following:
Final technique to perform tree shaking in esbuild is using conditional environment variable.
process.env.NODE_ENV !== 'production' && function initDevLogger() {
// some code
}
When NODE_ENV=production
then this is removed from the final bundle.
Material UI tree shaking example:
Here is an example comparing the final bundle size when ignoring the optimization annotations provided by the library. The difference is ~55% reduction (gzipped) in bundle size, and esbuild offers this type of reduction out of the box (like many modern bundlers today).
Tree map visualization of bundle size with esbuild out of the box (minify, production):
Final bundle size: ~ 275kb and 90kb (gzip)
Tree map visualization of bundle size with esbuild out of the box (minify, production) with annotations turned off:
Final bundle size: ~ 652kb and 202kb (gzip)
esbuild offers this out of the box but material ui does add in annotations into their libraries to provide optimzation hints to the bundler. Full example can be found here.
I personally recommend not to do manual tree shaking (ie adding annotations manually) unless you are truly confident that there will be no side effects in the modules that you build. Things like this should be built in at the bundler level. These techniques can be error prone.
Finally, I think knowing how the tree shaking process works helps you to build better component libraries or utilities in a way that can tap into these bundler optimizations (which are standard across most bundlers - wepback, rollup etc) without the unwanted side effects or work arounds.
esbuild supports running in the browser as well through esbuild-wasm.
Since esbuild transpiles so fast, I can see this open the doors of possibilities. It would likely be mostly used in development but I can also see specific applications like web and online code editors needing just in time compilation from source files.
I mentioned esbuild supports both an api in js and go. The documentation also provides various examples with configuration for the various feature set.
So, if your team is big on golang, then I don’t see how you can say no ;)
Fetch the package:
go get github.com/evanw/esbuild/pkg/cli
Initialize the esbuild build file in go:
// build.go
package main
import (
"os"
"github.com/evanw/esbuild/pkg/cli"
)
func main() {
os.Exit(cli.Run(os.Args[1:]))
}
Run the build:
go run build.go ./index.ts --outfile=index.js --bundle --format=cjs
Also, plugins can be written in go as well.
This is a simplified example for demonstration purposes but you can certainly have a more complex setup, refer to the api docs.
Is this ready for production ? I’d say so for the most part but it may depend on your requirements, and browser support required. So, it may not quite be there yet in some use cases.
The trade off with using esbuild is that the feature is slim. On the other hand, webpack will handle majority of your cases in getting your bundle ready for the browser.
At this time, I think esbuild is a great fit if you want to be using Typescript in your node applications (servers, serverless functions ie AWS lambda), shared utility and libraries.
These are the things I am using them for, and I haven’t really hit any major roadblocks of strange issues or errors in compilation. As I mentioned, there may be syntax that you may be using that are not be supported yet or you may run into an edge issue so just test it out and see how it goes.
In my opinion, the simplicity, and speed is worth it.
I really think esbuild is a great tool to have in your tool box for quickly getting a setup ready to bundle typescript files whether it is for a node application (api, lambda) or a utility function.
The simplicity of the tool is refreshing, the speed is mind boggling and the documentation is clear as day.
Evan has truly shared a gem of a tool in hopes to inspire others to build better tools within the ecosystem. I believe it has, and this is just the start of the next frontier. Esbuild has set the bar really high. It may be difficult to top its performance but I remain hopeful ;).
We are about to see massive innovation and improvements coming in the JS/TS tooling space. I hope it trends towards the simplicity in design that esbuild has gone for.
It really is the swiss army knife of the modern web. So, I recommend giving it a try!
If you are hands on like me, feel free to check out my esbuild-example repo. Play around with the examples, and see for yourself.
Then consider signing up to get notified when new content arrives!