How JavaScript works: A deep dive into esbuild

Lawrence Eagles
SessionStack Blog
Published in
12 min readMay 12, 2022

This is post #62 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript tool for developers to identify, visualize, and reproduce web app bugs through pixel-perfect session replay.

Introduction

Esbuild is a blazing fast JavaScript bundler built with Golang. Esbuild is a next-generation JavaScript bundler aimed at improving build tool performance and ease of use. And some of its main features are:

Although esbuild does not have a robust feature-set like Webpack, it does its job well — it is easy to use and blazing fast. The image below shows a performance comparison between esbuild and other leading JavaScript bundlers:

According to esbuild’s official website, the above image shows the time to do a production bundle of 10 copies of the three.js library from scratch using default settings, including minification and source maps. More info here.

We can see from the image above that it took esbuild 0.33 seconds to perform this task, and Webpack 5 took 41.53 seconds to perform this task.

While esbuild outperforms other JavaScript bundlers, it is important to mention a downside. And this is because performance is a tradeoff, so while esbuid is lightning-fast, it is not as feature-packed as Webpack.

Also, esbuild has not reached version 1.0, and although esbuild is rising in popularity, it is still less popular than other established bundlers like Webpack and Rollup, as seen in the npm trend below:

However, esbuild’s minimal feature set means it is not as complex as Webpack. esbuild is simple to learn and use, and it is blazing fast.

In addition to the caveats above, esbuild provides APIs for JavaScript, CLI, and Golang.

And in this article, we will learn about esbuild and how to bundle JavaScript applications with it.

Let’s get started in the next section.

Getting Started

Before delving into code, we will learn some esbuild concepts such as the esbuild content types and build API.

Content Types

Esbuild provides built-in support for different content types using loaders. These loaders are similar to Webpack loaders in functionality — they tell esbuild how to parse each content type. Some of these loaders: the TypeScript loader, the JavaScript loader, and the CSS loader are configured by default. And this means esbuild provides out-of-the-box support for these content types.

Below is a list of the content types supported by esbuild:

  • Javascript: The JavaScript loader is enabled by default for .js, .cjs, and .mjs files.
  • TypeScript: The TypeScript loader is enabled by default for .ts, .tsx, .mts, and .cts files. And this enables esbuild to provide built-in support for parsing TypeScript syntax and discarding the type annotations. However, esbuild does not perform type-checking.
  • JSX: The JSX loader is enabled by default for .jsx and .tsx files. However, JSX syntax is not enabled in .js files by default. But we can enable configure this using the build API as seen below:
    https://gist.github.com/lawrenceagles/2d53816f6c58b6c34d6e17d0eb0a897e

We will learn more about the esbuild build API in a subsequent section.

  • JSON: This loader parses JSON files into JavaScript objects and exports that object by default. It is also enabled by default for .json files.
  • CSS: In esbuild, CSS is first-class content-type and this means esbuild can bundle CSS files directly without needing to import your CSS from JavaScript code. As seen below:
    https://gist.github.com/lawrenceagles/2a394cf3da5780a2f558df37a24ca889

So this loader is enabled by default for .css files and it loads these files as CSS syntax.

  • Text: This leader is enabled by default for .txt files. The text loader loads the file as a string at build time and exports the string as the default export. And it provides a simple API as seen below
    https://gist.github.com/lawrenceagles/bd9b8189dbb08e3d65476fb4e0410a8e
  • Binary: This loader loads the file as a binary buffer at build time and embeds it into the bundle using Base64 encoding. However, this loader is not enabled by default.
  • Base64: This loader loads the file as a binary buffer at build time and embeds it into the bundle as a string using Base64 encoding. This loader is also not enabled by default.
  • Data URL: This loader loads the file as a binary buffer at build time and embeds it into the bundle as a Base64-encoded data URL. The data URL loader is useful for bundling images and it can be used in tandem with the CSS loader to load images using url().
  • This loader is not enabled by default. And to use it, we need to configure it for the appropriate file extension, as seen below:
    https://gist.github.com/lawrenceagles/71dbee9cd7393515f8db283db005c75a
  • External file: This loader copies files to the output directory and embeds the file name into the bundle as a string. And this string is exported using the default export. Similar to the data URL loader this loader can be used to load images and can work together with the CSS loader.
  • To use this loader we need to manually configure it for the appropriate extension, as seen below:
    https://gist.github.com/lawrenceagles/6c1121af845829b4f8875af454a244eb
    And using it looks like this:
    https://gist.github.com/lawrenceagles/b568cc5c02930a16d7bd39528782907a

The Build API

Although we can use esbuild via the terminal using the CLI API, if to pass many options to the CLI it can become unwieldy. So for more sophisticated use cases, esbuild also provides a JavaScript API that is the build API. And this allows us to customize the behavior of esbuild. It is synonymous with the webpack.config.js file for Webpack.

To esbuild build API looks something like this:
https://gist.github.com/lawrenceagles/8be4b1bd951e0b433daf804d3d825d2a

The build function runs the esbuild executable in a child process and returns a promise that resolves when the build is complete.

Although esbuild provides an alternative build API: buildSync — that runs synchronously it is best to use the async build API because esbuild plugins only work with the asynchronous API.

In an advanced case where we want to support old browsers, we need to transform modern JavaScript syntax into older JavaScript syntax.

We can configure the target environment as seen below:
https://gist.github.com/lawrenceagles/aeca2ca9bcf7869ab92dbd872b9f0c4a

Note the example above uses the buildSync API.

The esbuild build API provides us with many simple and advanced options for customizing the behavior of esbuild.

And in the code above, we have used some of these options:

  • Entry points: This option is an array of files. And each file serves as an input to the bundling algorithm. They are called entry points because they are evaluated first, then they load all other code in the app.
    So instead of loading many libraries on your page with <script> tags, we can use the import statements to add them to our app’s entry point.
  • Outfile: This option is only applicable if there is only a single entry point as seen in our example above. The outfile option specifies the name of the final bundle — the output file created by the build process.
  • When there are multiple entry points, we must use the outdir option to specify an output directory.
  • Outdir: This option specifies an output directory for the build process. And this directory will be created only if it does not already exist. For example, the code below would create an output directory called output for the build operation:
    https://gist.github.com/lawrenceagles/fea875722e3b92874c71516bc78be45d
  • Bundle: esbuild does not bundle by default, so to bundle our file we need to explicitly specify it as seen above by setting its option to true.
  • Minify: When set to true this option enables the minification of our code during the build process. Minified code is smaller than pretty-printed codes, and they are easier to download. But minified code is more difficult to debug, so usually, we minify code when we are building for production.
  • Sourcemap: A sourcemap is a file that provides a way to map minified and uglified JavaScript bundle into its unbundled state. During the built state application assets — CSS & JavaScript files get minified and combined into a single bundle to make delivering them from the server more efficient. However, these minified and uglified bundles are difficult to read and debug. Sourcemaps is a file that maps from the bundled source code to the original — unbundled source code thus enabling the browser to reconstruct the unbundled source and deliver it in the debugger. By setting this option to true we tell esbuild to generate sourcemaps.
  • Target: This specifies the target environment — like the browser, for the bundled JavaScript and/or CSS code. So if the JavaScript syntax is too new for the specified environment, it tells esbuild to transform it into older JavaScript syntax that can work in these environments.

Apart from these, there are more options like watch, serve, and other advanced options such as tree shaking, JSX fragments, JSX factory, etc.

Bundling with esbuild

In this section, we will learn how to bundle applications with esbuild.

To use esbuild, first, create a nodejs project by running:

npm init -y

From your project directory. Then install the esbuild package by running:

npm install esbuild

You can verify the version by running:

/node_modules/.bin/esbuild — version

And this prints: 0.14.38

We will be bundling a React application so install the following React packages:

npm install react react-dom

Now create an app.jsx file containing the following code:
https://gist.github.com/lawrenceagles/4829768fab37f3839874610d6504c97a

Now we can tell esbuild to bundle our application using the CLI API by running:

./node_modules/.bin/esbuild app.jsx — bundle — outfile=bundle.js

And we get:

So by running the command above, esbuild bundles our app into a bundle.js file. Also, esbuild converts the JSX syntax to JavaScript without any configuration other than the .jsx extension.

There are two things to note from our example above:

  1. The esbuild build process does not bundle our app by default, so we explicitly need to pass the — bundle flag in the command. And where the
    — bundle flag is absent, esbuild would run the Transformation API instead of the build API.
    The esbuild transformation API is ideal for environments such as the browser — that don’t have a file system because it performs operations on a single string without access to the file system. And in the case above, by running:
    ./node_modules/.bin/esbuild app.jsx — outfile=bundle.js
    The transformation API is called because the — bundle flag is absent, and our code is transformed into a bundle.js file with the following code:
    https://gist.github.com/lawrenceagles/ca983900b7189d075cd807654594fb2e
    Thus we can see that the transformation API transformed our JSX syntax into pure JavaScript.
  2. The second thing to note is that by default, esbuild does not allow JSX syntax in .js files, so we had to name our file app.jsx. But if we rename our app.jsx file to app.js and try to build or transform our app we get an error as seen below:

And while this can be fixed by adding the loader flag: — loader:.js=jsx to the CLI command, we can also do this using the build API.

So rename the app.jsx file to app.js and create a buid.js file containing the following codes:
https://gist.github.com/lawrenceagles/1c71b91cd981df752d430db3391b4be5

Then update the package.json script as seen below:
https://gist.github.com/lawrenceagles/7981b3be6b5b7dac04fbe9d11fc26490

Now we can build our app by running:
npm run build

Also, setting the bundle option to false tells esbuild to use the transformation API instead of the build API.

Plugins

Esbuild plugins enable developers to hook into the build process and perform operations. And unlike the build API and the Transformation API, the esbuild plugin API is not available from the CLI. Thus you can only use them via the build API.

And it is important to keep in mind that esbuild plugins do not work with the buildSync API as mentioned above.

One drawback with using the plugin API is that it is not mature. And accounting for the documentation:
“The plugin API is new and still experimental. It may change in the future before version 1.0.0 of esbuild as new use cases are uncovered. You can follow the tracking issue for updates about this feature.”

With this in mind, various plugins are developed by the community, but these are not officially supported. Here is a list of these plugins.

Conclusion

In the article, we have learned a lot about esbuild the next-generation and blazing fast JavaScript bundler.

Esbuild is minimal featurewise if compared to bundlers like Webpack. However, it trades these features for optimal performance. So this should be the key thing to consider before adopting esbuild.

But this is resolved by using a package like Vite. Vite is a feature-rich Webpack alternative. Vite uses esbuild under the hood — in a smart way. And if you are new to Vite, you can learn all about Vite in our previous article in this series.

Blazing-fast next-generation JavaScript bundlers like esbuild make it much easier for software to have efficient and high-performing code. esbuild has not reached version 1.0 and its API is still evolving. But since we all like to apply new technologies & upgrade our code so even if we feel we’ve tested everything before release, it’s always necessary to verify that our users have a great experience with our product.

A solution like SessionStack allows us to replay customer journeys as videos, showing us how our customers actually experience our product. We can quickly determine whether our product is performing according to their expectations or not. In case we see that something is wrong, we can explore all of the technical details from the user’s browser such as the network, debug information, and everything about their environment so that we can easily understand the problem and resolve it. We can co-browse with users, segment them based on their behavior, analyze user journeys, and unlock new growth opportunities for our applications.

There is a free trial if you’d like to give SessionStack a try.

SessionStack replaying a session

If you missed the previous chapters of the series, you can find them here:

--

--