This article was last updated on November 12, 2024, to add sections on Debugging and Troubleshooting esbuild Issues, as well as Security and Privacy Considerations for using 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
- Features of esbuild
- Comparison with other bundlers
- Why it is so fast
- Example esbuild Usage
- Advanced Configuration Options
- Is it Production ready?
- esbuild TypeScript support
- Performance Optimization
- Security and Privacy Considerations
- Integration with CI/CD and Version Control
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 thatJSX
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.
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:
- 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));
- 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));
- 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:
- 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));
- Third-Party Plugins:
- Visualization of the bundle size can also be done through third-party plugins, for instance,
source-map-explorer
.
- Visualization of the bundle size can also be done through third-party plugins, for instance,
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
Security and Privacy Considerations
When using esbuild or any other development tool, prioritizing security and privacy is crucial, especially in collaborative or production environments. Although esbuild is generally secure, there are some key areas where additional vigilance can strengthen your project’s security.
- Dependency Management:
Since esbuild can bundle dependencies from npm or other package managers, ensuring that these dependencies are trustworthy is essential. Third-party packages can introduce vulnerabilities if they’re compromised or out-of-date. Regularly use tools like npm audit or yarn audit to scan for and address potential vulnerabilities in your dependencies. This proactive approach helps you catch and update vulnerable packages before they impact your application.
- Access Control:
In team projects, especially those with third-party contributors, limit access to sensitive build configurations. Avoid hardcoding sensitive keys, tokens, or secrets directly into configuration files. Instead, use environment variables and secure storage solutions like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. This approach not only keeps sensitive data secure but also simplifies the rotation and management of these credentials without altering code.
- Code Minification and Obfuscation:
For projects that are publicly distributed or deployed, consider adding an extra layer of protection through minification and obfuscation, especially if your code contains proprietary algorithms or sensitive logic. Esbuild provides minification by default, but you may want to consider additional tools or techniques for obfuscation. While this won’t make your code entirely secure, it can add complexity for anyone attempting to reverse-engineer it.
- Regular Security Reviews:
Conduct periodic security reviews of your build process, especially if your project requirements change or grow. Such reviews should evaluate dependency updates, access permissions, and any security configurations in place. Having a routine security audit process ensures your project adapts to evolving security threats.
By focusing on secure dependency management, proper access control, code obfuscation, and regular reviews, you can better protect your project and its data when using esbuild in any environment.
- Integration with CI/CD and Version Control
Integrating esbuild into a CI/CD pipeline can streamline your workflow, automating build, test, and deployment processes to increase efficiency and reduce manual errors. Setting up esbuild in CI/CD environments like GitHub Actions, GitLab CI, or Jenkins is relatively straightforward, enabling faster, consistent builds.
- Setting Up esbuild in CI/CD:
- First, add esbuild as a project dependency, and create a build script in your package.json file to simplify the build command:
"scripts": {
"build": "esbuild src/index.ts --bundle --outfile=dist/bundle.js --minify"
}
In your CI/CD configuration file (e.g., .github/workflows/build.yml for GitHub Actions), set up a job to install dependencies and run the build command:
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm install
- run: npm run build
This setup triggers a build on every push to the main branch, ensuring that the latest changes are consistently bundled and ready for deployment. It’s also adaptable to include testing steps, linting, or even deployment commands for a more complete CI/CD process.
- Source Maps and Debugging:
For better debugging in production, enable source maps in esbuild by adding the --sourcemap flag to your build command. This lets you trace errors back to the original source code, which is invaluable for troubleshooting production issues. Source maps can be stored securely in your deployment environment or connected to error-tracking services to improve error handling.
- Version Control Best Practices:
Esbuild’s incremental build mode is ideal for local development but can introduce inconsistencies in CI/CD environments. Therefore, for a CI/CD setup, perform a clean build each time to ensure consistency. This helps avoid build issues caused by artifacts from previous runs and aligns with best practices for creating reliable builds.
- Efficiency and Consistency in Builds:
Incorporating esbuild into a CI/CD pipeline standardizes the build process, enabling faster and more consistent releases. Regular, automated builds reduce manual intervention, ensure that code changes are always tested and bundled correctly, and provide a reliable output for each deployment. CI/CD tools can also handle version tagging, rollback, and notification, further strengthening the workflow.
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:
- 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));
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.
Debugging and Troubleshooting esbuild Issues
As your project grows, integrating additional tools and dependencies with esbuild can sometimes lead to unexpected issues. Here’s a guide on common problems and techniques for effectively debugging and troubleshooting esbuild.
Checking Error Messages
The first step is to examine error messages closely. Esbuild provides helpful, specific error messages, often pinpointing the exact line and file of the issue. Errors like syntax issues or unsupported features are usually identified by esbuild, so always start by reviewing the terminal output for insights into what went wrong.
Enabling Source Maps
Source maps are vital for debugging, especially in production or complex applications. By adding the --sourcemap
flag, you can trace errors back to the original code, simplifying troubleshooting:
esbuild src/index.ts --bundle --outfile=dist/bundle.js --sourcemap
Source maps allow you to inspect your code directly in your browser’s developer tools, making it easier to locate errors in bundled files.
Using Logging and Verbose Output
For complex builds, esbuild’s default output may not provide enough information. Increasing verbosity with the logLevel option or adding logging statements can offer more insight:
require("esbuild").build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/bundle.js",
logLevel: "debug", // Options include 'silent', 'info', 'warning', 'error', 'debug'
}).catch(() => process.exit(1));
Setting logLevel to "debug" provides detailed output, which helps in understanding build behavior and identifying misconfigurations.
Resolving “Module Not Found” Errors
If you encounter a “Module Not Found” error, check that: • The dependency is correctly installed by running npm install or yarn install. • The node_modules folder includes the required package. • Custom paths or aliases are correctly configured in your tsconfig.json or jsconfig.json.
For instance, if you’re using an alias like @components, ensure it’s defined in your configuration and recognized by esbuild.
Managing Compatibility Issues with Plugins
Esbuild is designed for speed but may lack support for certain features by default. For example, if you’re working with non-standard syntax or specific integrations (e.g., Sass or SCSS), you may need plugins:
require("esbuild").build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/bundle.js",
plugins: [require("esbuild-plugin-sass")()], // Add necessary plugins
}).catch(() => process.exit(1));
Plugins can extend esbuild’s functionality, making it more compatible with various project requirements.
Checking for Version Incompatibilities
Occasionally, version mismatches between esbuild and other dependencies can cause errors. Review esbuild documentation for version requirements related to Node.js, TypeScript, or other tools in your stack. Ensure all packages are current by running:
npm update
Keeping dependencies up to date helps prevent conflicts and ensures compatibility across tools.
Using Incremental Builds for Faster Debugging
If you’re frequently making code changes, esbuild’s incremental build mode rebuilds only modified files, reducing build times and allowing faster iteration:
require("esbuild").build({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/bundle.js",
incremental: true, // Enable incremental mode
}).catch(() => process.exit(1));
Incremental builds are especially beneficial during active development.
Handling Performance Issues
For large projects, esbuild might run slower due to many dependencies or large files. To improve performance, enable tree-shaking (to remove unused code) and use minification for production builds:
esbuild src/index.ts --bundle --outfile=dist/bundle.js --minify
Tree-shaking and --minify optimize bundle size and performance, which is particularly helpful as your project scales.
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.