Bundling JavaScript applications takes time and can be complicated. A single bundling process doesn’t take a lot of time, but in your development process, the bundling processes add up and they can add a significant delay to your development pipeline.
On top of that, bundling a JavaScript application usually requires you to write a configuration file. If you bundle a JavaScript application using webpack
, you need to write webpack.config.js
. It’s a significant cognitive overhead.
That’s where esbuild comes in. Esbuild is a fast and simple JavaScript bundler written in Go.
In this article, you’ll learn how to use esbuild to bundle JavaScript applications. You’ll explore common use cases, from bundling TypeScript, React, image files, and CSS files to serving the bundling process as a server.
First, install the bundler using npm:
$ npm install -g esbuild
Then you can verify the installation by invoking esbuild:
$ esbuild --version 0.13.12
If you don’t want to install esbuild globally, you can do that as well:
$ npm install esbuild
But you have to invoke esbuild with a full path:
$ ./node_modules/.bin/esbuild --version 0.13.12
The first task you’re going to accomplish using esbuild is bundling a TypeScript file. Create a file named input_typescript.ts
and add the following code to it:
let message: string = "Hello, esbuild!"; console.log(message);
You can bundle the TypeScript code via CLI:
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts output.js 99b ⚡ Done in 7ms
Then, check the content of the bundled file like so:
(() => { // input_typescript.ts var message = "Hello, esbuild!"; console.log(message); })();
The esbuild command accepts input_typescript.ts
as the argument. We’ll refer to this argument as the entry point, because it’s where the application starts.
Then, provide the outfile
option as a way to define the output file. If you don’t provide this option, esbuild will send the result to stdout. The loader
option is the one that you use to load the TypeScript file extension. You can omit this option, however, because esbuild can decide which loader to use based on the file extension.
With the bundle option, esbuild
will inline all dependencies into the output file. Let’s look at a demo to see the difference.
Suppose you have a file named main.ts
with the content as follows:
import { SayHello } from "./library"; SayHello();
The main.ts
file imports SayHello
from library.ts
which has the content as below:
export function SayHello() { console.log("Hello, esbuild!"); }
If you don’t use the bundle
option, esbuild will just import the dependency in the result:
$ esbuild main.ts import { SayHello } from "./library"; SayHello();
But if you used the bundle
option, esbuild would inline the content of the library in the result:
$ esbuild main.ts --bundle (() => { // library.ts function SayHello() { console.log("Hello, esbuild!"); } // main.ts SayHello(); })();
With the bundle
option, you pack all your code into one file. In other words, two files become one file.
Integrating React library into your project is a complicated venture. It even warrants the creation of a Create React App project. If you want to use webpack to add React into your project, you have to endure the writing process of a complicated webpack.config.js.
But with esbuild, it’s a simple process.
First, install the React library using npm:
$ npm install react react-dom
Then create a JavaScript file called App.js
. Add the following code to the file:
import React from "react"; import ReactDOM from "react-dom"; function App() { return ( <div>Hello, esbuild!</div> ); } ReactDOM.render(<App />, document.getElementById("root"));
Create an HTML file called index.html
so React can render your application into the div with an ID root. Add the following code to the file:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Hello, esbuild!</title> </head> <body> <div id="root"></div> <script src="AppBundle.js"></script> </body> </html
In the HTML file, we are using AppBundle.js
. This is the name of the bundled JavaScript file.
Now, bundle App.js
to AppBundle.js
:
$ esbuild App.js --bundle --outfile=AppBundle.js --loader:.js=jsx AppBundle.js 890.8kb ⚡ Done in 46ms
You’ve learned all the options in the previous section. You use the bundle
option because, well, you want to bundle the JavaScript file. Then, give the output file the name you want using the outfile
option.
The last option, loader
, is not actually optional. Tell esbuild to use the JSX loader for files with the .js extension, because JSX syntax is inside App.js
. If you don’t use the JSX loader, esbuild will throw an error. You can omit the loader option if the extension of the input file is .jsx, not .js. So if you name the JavaScript file App.jsx
, then you can omit the loader
option.
Now that you have AppBundle.js
, let’s open index.html
to check whether your bundling process works or not. You must open index.html
using the http protocol, not the file protocol.
Then, you can serve the HTML file using http-server
:
$ npx http-server
Finally, open http://localhost:8080/index.html. You should see the screen below:
While you can bundle your JavaScript file with CLI, you also have an option to use the build API.
Suppose you want to bundle input_typescript.ts
into output.js
. This is the command you would use:
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts
Let’s try the build API. Write a JavaScript file called build.js
and add the following code:
require("esbuild").build({ entryPoints: ["input_typescript.ts"], outfile: "output.js", bundle: true, loader: {".ts": "ts"} }) .then(() => console.log("⚡ Done")) .catch(() => process.exit(1));
Import the esbuild library and use the build
method from it. The argument is an object that has keys and values similar to the options in the esbuild command.
Then you can execute the bundling process with Node.js:
$ node build.js ⚡ Done
You can treat the build
file as a configuration file. It’s like webpack.config.js
, but for esbuild.
Let’s try bundling something else, such as CSS files. Create a CSS file named color.css
and add the following code to it:
.beautiful { color: rgb(0,0,255); }
Then, create another CSS file that imports the CSS file above. Name it style.css
and add the following code to it:
@import 'color.css'; p { font-weight: bold; }
To bundle these two CSS files, you can use esbuild as shown below:
$ esbuild style.css --outfile=out.css --bundle out.css 100b ⚡ Done in 7ms
The content of out.css
will be the combination of the two CSS files:
/* color.css */ .beautiful { color: rgb(0, 0, 255); } /* style.css */ p { font-weight: bold; }
Now, you can include only this one file in your HTML file.
You can also minify the CSS file using the minify
option:
$ esbuild style.css --outfile=out.css --bundle --minify out.css 42b ⚡ Done in 3ms
The content of the CSS file will be compact, as shown below:
.beautiful{color:#00f}p{font-weight:bold}
As you can see, the bundler even changed the way you specify the color. The input file uses the rgb syntax, but the output file uses hexadecimal code, which is more compact.
You can also bundle images with esbuild. You have two options for bundling images: the first is to load the image as an external file in the JavaScript file, and the second is to embed the image as a Base64-encoded data URL in a JavaScript file.
Let’s look at the difference. First, put one JPG file and one PNG file into the project directory. You need two images with different extensions because you want to load both images in different ways. Name the PNG image image.png
and the JPG image image.jpg
.
Create an HTML file named images.html
and add the following content:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Hello, esbuild!</title> </head> <body> <div id="root"> <div> <img id="image_png" /> </div> <div> <img id="image_jpg" /> </div> </div> <script src="out_image.js"></script> </body> </html>
Then, you need to create a JavaScript file. Name it input_image.js
and add the following code:
import png_url from './image.png' const png_image = document.getElementById("image_png"); png_image.src = png_url; import jpg_url from './image.jpg' const jpg_image = document.getElementById("image_jpg"); jpg_image.src = jpg_url
Next, load the image using the import statement inside the JavaScript file. Unlike bundling CSS files, you don’t bundle images directly, but bundle images by bundling the JavaScript files that refer to the images.
Now, bundle the JavaScript files:
$ esbuild input_image.js --bundle --loader:.png=dataurl --loader:.jpg=file --outfile=out_image.js out_image.js 20.1kb image-UKQOKISI.jpg 10.1kb ⚡ Done in 11ms
Notice that you used two loaders. The .png extension uses the dataurl
loader and the .jpg extension uses the file
loader. Instead of image-UKQOKISI.jpg
, you will get a different name.
If you peek inside out_image.js
, you’ll see the following:
(() => { // image.png var image_default = "data:image/png;base64,iVBORw0KGgoAAAANSU..." // image.jpg var image_default2 = "./image-UKQOKISI.jpg"; // input_image.js var png_image = document.getElementById("image_png"); png_image.src = image_default; var jpg_image = document.getElementById("image_jpg"); jpg_image.src = image_default2; })();
As you can see, the first image uses a Based64-encoded data URL format. The second image uses the file path format. For the second image, you also have an external file called image-UKQOKISI.jpg
.
You can check the images by opening images.html
:
$ npx http-server
Open http://localhost:8080/images.html and you would get the following screen:
Esbuild is not a complete solution for bundling. It has default supports for React, CSS, and images, but it doesn’t support SASS. If you want to bundle SASS files, you need to install an esbuild plugin. The list of the esbuild plugins can be found here.
There are a couple of plugins that bundle SASS files. In this tutorial, you’ll use esbuild-plugin-sass
. Install the plugin using npm like so:
$ npm install esbuild-plugin-sass
Let’s create an SCSS file named style.scss
. Add the following content to it:
$font: Roboto; $color: rgb(0, 0, 255); #root { font: 1.2em $font; color: $color; }
To use the esbuild-plugin-sass
plugin, you need to use the build API. Create a file called sass_build.js
and add the following content:
const sassPlugin = require("esbuild-plugin-sass"); require("esbuild").build({ entryPoints: ["style.scss"], outfile: "bundle.css", bundle: true, plugins: [sassPlugin()] }) .then(() => console.log("⚡ Done")) .catch(() => process.exit(1));
Notice that you use the plugin using the plugins
key. The entry is the SCSS file, but you can also fill the entry with the JavaScript file, which imports the SCSS file. The output is the CSS file.
Execute this build file:
$ node sass_build.js ⚡ Done
You can check the result by opening the bundle.css
file:
/* ../../../../../../tmp/tmp-234680-cl7EYSZ4C0qM/esbuild_demo/style.css */ #root { font: 1.2em Roboto; color: blue; }
It’s not fun to execute the bundling process every time you modify the input file. There should be a way to bundle the input files automatically. For this case, esbuild has the watch
mode.
Create a file called watch_build.js
and add the following content:
require("esbuild").build({ entryPoints: ["input_typescript.ts"], outfile: "output.js", bundle: true, loader: {".ts": "ts"}, watch: true }) .then(() => console.log("⚡ Done")) .catch(() => process.exit(1));
The input_typescript.ts
file is the same as the previous example. This is the content of the file:
let message: string = "Hello, esbuild!"; console.log(message);
Execute the build file like so:
$ node watch_build.js ⚡ Done
The process hangs up. Check the content of output.js
:
(() => { // input_typescript.ts var message = "Hello, esbuild!"; console.log(message); })();
While the build process is still alive, change the content of input_typescript.ts
to the content shown below:
let message: string = "Hello, esbuild!"; let x: number = 3; console.log(message); console.log(x);
Finally, check the content of output.js
again:
(() => { // input_typescript.ts var message = "Hello, esbuild!"; var x = 3; console.log(message); console.log(x); })();
The output file is updated automatically. watch
watches the file system so esbuild can bundle the input files when it detects that the file changes.
There is another way to bundle files automatically called serve
mode. It means that you launch a server to serve the output file. If someone requests the output file from the browser, the server will bundle the input files automatically if the files have been changed.
Let’s create an HTML file called index_ts.html
and add the following code to it:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Hello, esbuild!</title> </head> <body> <script src="output.js"></script> </body> </html>
The output file is output.js
, and the user requests it indirectly by accessing index_ts.html
. The input file is the same as before, input_typescript.ts
. The content of the file is as follows:
let message: string = "Hello, esbuild!"; console.log(message);
This is how you bundle the file using the serve
mode:
$ esbuild input_typescript.ts --outfile=output.js --bundle --loader:.ts=ts --serve=localhost:8000 --servedir=. > Local: http://127.0.0.1:8000/
The serve
option is used to define the server and the port. The servedir
option defines the directory the server serves.
Now, open http://127.0.0.1/8000/index_ts.html and check the console:
Modify input_typescript.ts
into the following code:
let message: string = "Hello, esbuild!"; let x: number = 5; console.log(message); console.log(x);
Now, refresh the browser or open http://127.0.0.1/8000/index_ts.html again. You will see the following screen:
As you can see, the bundling process happened automatically.
In this article, you’ve learned how to use esbuild to bundle TypeScript, React, CSS, image files, and SCSS files. You used esbuild tool via CLI and the build API. You executed esbuild with different options according to your needs.
This article only scratches the surface of esbuild. There are many sides of esbuild that we haven’t covered, such as using sourcemap, injecting functions, and naming the assets. Please check the documentation to learn more. The code for this article is available on this GitHub repository.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.