Skip to main content
esbuild - Next-generation JavaScript bundler
12 min read

esbuild - Next-generation JavaScript bundler

This article was last updated on July 04, 2024, to add sections for Advanced Configuration Options and Performance Optimization to esbuild

Introduction

Bundlers are an essential component of the JavaScript ecosystem. As the name implies, bundlers generate one or more module bundles. Module bundlers process the JavaScript applications and build dependency graphs to map each module needed by your project. Generally speaking, bundlers perform the following tasks:

  • Bundle CSS, HTML, images, and other assets
  • Bundle JavaScript code in required module formats
  • Build optimizations, including code-splitting, scope-hoisting, tree-shaking, etc.
  • Hot module replacement during the development

Many bundlers are used in developing modern JavaScript applications, including Webpack, Rollup, and parcel, but we will discuss a relatively new entrant, esbuild, which is a very fast and efficient bundler.

Steps we'll cover:

Why another JS bundler

Technology is progressing very fast. Every day you will see new frameworks, build tools, and libraries to speed up and improve your software applications. esbuild is another example of constant innovation and improvement. It is an open-source next-generation JavaScript bundler that is very fast and more efficient than other bundlers in the industry. It is written in Go language with speed in mind; it is powered by parallel parsing, printing, and source map generation. It packages and bundles JS code for distribution on the web. Some of its features include:

  • It is very fast, even without any cache. It is much faster than other bundlers.
  • A robust API for JavaScript and Go
  • ES6 and common JS modules
  • Supports TypeScript and JSX syntax
  • Source maps
  • Minification

Features of esbuild

Let's go through some of its features in detail.

Bundling and supported content types

esbuild supports both bundling and code splitting. Bundling is when you want to deploy a single app.js target. Code splitting is when you want to split app.js into many targets, like header.js or sidebar.js etc. esbuild has built-in support for various content types using its component called "loaders". The loader tells esbuild how to parse a particular content type. The three common loaders enabled by default are:

  • Typescript loader
  • JavaScript loader
  • CSS loader

If we look at the content types supported by esbuild, then these are as below:

  • JavaScript Loader: As mentioned above, the JavaScript loader is enabled by default, and it supports .js. .cjs, and .mjs files
  • Typescript Loader: Enabled by default for .ts. .tsx. mts, and .cts files. However, it does not support type-checking.
  • JSX Loader: It is enabled by default for .jsx and .tsx files. Note that JSX syntax is not enabled in .js files by default. You can, however, enable this by updating the configuration.
  • JSON Loader: It is enabled by default for .json files. It parses JSON files to JavaScript objects and exports these objects.
  • CSS Loader: It can bundle CSS files directly without importing your CSS from the JavaScript code. This loader is also enabled by default.
  • Text Loader: It is also enabled by default for .txt files. It loads the files as a string during build time and exports the string default export.
  • Binary Loader: It loads the file in the form of a binary buffer at build time and includes it in the bundle as Base64 encoding. It is not enabled by default.
  • Data URL: It loads the file as a binary buffer at build time and embeds it into the bundle as a Base64 encoded data URL. This loader is useful for bundling images along with the CSS loader to load images using the method url(). It is not enabled by default.

The build API

esbuild has a powerful JavaScript build API through which you can customize the behavior of esbuild. It is similar to webpack.config.js file in the Webpack.
If you look at the code sample below, you can see that the build function executes the esbuild in a child process and returns a promise that is resolved when the build is complete.
Note that esbuild also provides a synchronous build API buildSync that runs synchronously. You will need to use the asynchronous build API because esbuild plugins are compatible with only asynchronous API.

require('esbuild').build({
entryPoints: ['app.jsx'],
bundle: true,
outfile: `bundle.js',
}).catch(() => process.exit(1))

Incremental compilation

esbuild supports incremental compilation. If you are compiling the same file from different sources again and again, esbuild will work only on changed sources instead of code splitting or bundling from scratch each time.

Plugins

The plugins API is a very useful feature of esbuild. It allows you to preprocess files when they are linked. It can be very beneficial if you are converting Sass to CSS or markdown to JSX etc. You can still configure the implementation details through the plugins API.

Server mode

The server mode enables you to use esbuild as a web server, and you can implement your own server handler for incoming requests. This feature is very powerful because you can use the server handler to perform different functions on incoming requests, like observe events and log them. esbuild utilizes code-split targets from memory instead of the disk to serve your bundled code, making it a highly performant web server as it reduces the total work spent on each request.

Watch mode

Watch mode means the esbuild can detect the changes in the source code as they occur. Instead of worrying about file-watchers or using libraries like Nodemon, or chokidar, etc. you can offload this responsibility to esbuild. In fact you can also implement your own watch handlers so you can log events, observe them and push server-sent events.

Comparison with other bundlers

If you look at the below comparison between different bundlers, you can see that esbuild has a significant performance advantage over its competitors. Image a large team with many projects and dependencies where reducing build times is crucial for product development. The magic lies in the ability of esbuild to parallelize printing, parsing, and source map generation.

esbuild Source - https://esbuild.github.io/

Why it is so fast

Here is how esbuild is able to achieve this performance:

  • It is developed using the Go language, which compiles to native code. Other bundlers are mostly written in JavaScript, and NodeJS has to spend extra time to parse the JavaScript in case of other bundlers.
  • It is able to perform printing, parsing, and source map generation in parallel. The algorithms in esbuild are developed to fully saturate all CPU cores when possible. Note that parallelism is at the heart of Go language, and Go can make intelligent use of memory utilization as compared to JavaScript.
  • Everything is done in very few passes instead of expensive data transformation.
  • It has not too many features like Webpack, and its main focus is speed.

Example esbuild Usage

First, you need to create a NodeJS project by running this command

npm init –y

Go to your project directory and install the esbuild package by running the below command:

npm install esbuild

To verify if esbuild is correctly installed, run the below command, and it will return the esbuild version:

/node_modules/.bin/esbuild — version

This example uses using React application, so you need to run the following command to install react packages:

npm install react react-dom

Create an app.jsx file having the following code:

import * as React from "react";
import * as Server from "react-dom/server";

let Greet = () => <h1>Hello, esbuild Users</h1>;

console.log(Server.renderToString(<Greet />));

Now let's ask esbuild to bundle this application by running the below command:

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

What esbuild does here is that it bundles your application into bundle.js, and the whole process is extremely fast.

Advanced Configuration Options

I added in another section for the advanced options of configuration of our article on esbuild, which explains how we can modify the process of creating a build with esbuild: different entry points, formats of output, and target environments. I added examples of esbuild configuration for different use cases.

Customizing Build Process

esbuild provides flexible options to customize the build process, allowing developers to set different entry points, output formats, and target environments. These customizations enable esbuild to be used in a variety of scenarios, from creating library bundles to optimizing builds for specific environments.

Here are some of the examples of configuring esbuild for different use-cases:

  1. Creating a Library Bundle

If you're developing a library, you will probably want to output both ESM and CJS simultaneously. Here's how you might configure build to do this:

require("esbuild")
.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
target: ["es2020"], // Target environment
format: "esm", // Output format
})
.then(() => {
console.log("ESM build complete");
})
.catch(() => process.exit(1));

require("esbuild")
.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
sourcemap: true,
outdir: "dist",
target: ["es2020"], // Target environment
format: "cjs", // Output format
})
.then(() => {
console.log("CJS build complete");
})
.catch(() => process.exit(1));
  1. Optimizing Builds for Specific Environments

In the case of web applications, you may need to optimize the build for modern browsers or a specific JavaScript environment. Here is an example of setting up build for a web application:

require("esbuild")
.build({
entryPoints: ["src/app.jsx"],
bundle: true,
minify: true,
sourcemap: true,
outfile: "dist/app.js",
target: ["chrome58", "firefox57", "safari11", "edge16"], // Target modern browsers
define: {
"process.env.NODE_ENV": '"production"',
},
})
.then(() => {
console.log("Web application build complete");
})
.catch(() => process.exit(1));
  1. Create Node.js Application Bundle

While building for a Node.js environment, you might want to specify a different target and use external dependencies. Here's how to configure esbuild for a Node.js application:

require("esbuild")
.build({
entryPoints: ["src/server.ts"],
bundle: true,
platform: "node", // Target Node.js environment
target: ["node14"], // Specify Node.js version
outfile: "dist/server.js",
external: ["express"], // Exclude dependencies to be resolved at runtime
})
.then(() => {
console.log("Node.js application build complete");
})
.catch(() => process.exit(1));

Examples include the way esbuild can be configured to cater for building a library, a web application, or even a Node.js server. With this, you can optimize your project for various environments and use cases.

Is it Production ready?

esbuild is a great tool with a lot of potentials, however, it is still a small project maintained by a single person. There are not a lot of open-source contributions to this project, and its author is the only person maintaining it. While esbuild shows great performance compared to its counterparts, being a new entrant, you will not see many projects in production with esbuild yet. It is better to test it on a side project and push it to production after it goes well for your need.

esbuild TypeScript support

For TypeScript-based projects in production, you can take advantage of using tsup.

Using tsup you can build your TypeScript applications with minimal configuration.
It uses esbuild behind the scene so you get the power of esbuild along with the convenience of tsup. We, at Refine, have seen remarkable performance using tsup in our project dev/build processes.

Performance Optimization

I've made a fresh section in the performance optimization of our build article. It should cover bundle size analysis and advanced minification using in-built and third-party build tools/plugins for performance improvement.

Analyzing Bundle Size

Optimizing your JavaScript application bundle size is an important process that can help you improve your load time and overall performance. esbuild offers in-built tools with additional support for third-party plugins to analyze, reduce, or compress bundle size.

To analyze bundle size and optimize it using esbuild:

  1. Built-in Tools:
    • esbuild outputs statistics about the build contents, which helps know the composition of the bundle—including the size of each module and asset contained in a bundle.
const esbuild = require("esbuild");

esbuild
.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
sourcemap: true,
outfile: "dist/bundle.js",
logLevel: "info", // Provides detailed output statistics
})
.catch(() => process.exit(1));
  1. Third-Party Plugins:
    • Visualization of the bundle size can also be done through third-party plugins, for instance, source-map-explorer.
npm install source-map-explorer
// After building with esbuild, use source-map-explorer to analyze the bundle
// Command to run: source-map-explorer dist/bundle.js

Advanced Minification Techniques

Advanced minification can optimize your JavaScript bundle significantly through file reduction and the removal of redundant code. Here are performance enhancement configurations of esbuild that are supported to minify:

Example: Enable and Set Up Advanced Minification esbuild:

  1. Enabling Minification:
    • The minification can be enabled in esbuild through build configuration: whitespace is stripped, variable names are shortened, and dead code is eliminated.
esbuild
.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true, // Enable minification
sourcemap: true,
outfile: "dist/bundle.js",
})
.catch(() => process.exit(1));
  1. Configuring Minification Options:

    • Minification: Additional settings can be set to minify the bundle further; these include being able to exclude console statements, debugging information, and more.
esbuild
.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
sourcemap: true,
outfile: "dist/bundle.js",
define: {
"process.env.NODE_ENV": '"production"', // Remove development-specific code
"console.log": "null", // Remove console.log statements
},
})
.catch(() => process.exit(1));

We can significantly enhance the performance of our JavaScript applications with esbuild by analyzing and optimizing bundle size, which uses cutting-edge minification.

Conclusion

The world of JavaScript has a lot of great frameworks and tools. You will see many bundlers in the market, but esbuild is gaining a lot of popularity due to its amazing speed. In this article, we compared some top bundlers being used. We also discussed some of the core features of esbuild and how it delivers blazing-fast builds.

We also went through some basic commands for installing and building projects with esbuild. esbuild has a lot of future, and although it is a new kid on the block, it holds tremendous potential for organizations that want to build applications quicker and faster.