Bun.js is an exciting new project in the JavaScript ecosystem that offers a comprehensive runtime and toolchain. I've previously written a general introduction to the project as a drop-in replacement for Node.js. This article focuses on Bun's new bundling feature. While it's still in early releases, the Bun bundler is quite capable and flaunts Bun's speed. It could easily replace tools like WebPack, Vite, and Snowpack.
Let’s check out the new bundling support in Bun.
Why another bundler?
The first question for many developers is why we need another bundler. We already have a few of them, after all.
So what's special about the Bun bundler? One advantage is that having a bundler in the Bun toolchain brings everything under one roof. Rather than building a toolkit from parts, you've got everything you need in one toolchain. The other advantage is that Bun is built for speed, which matters for bundling. Bun is built on Zig and JavaScript to support faster built times. When magnified across development and devops (CI/CD) workflows, Bun's performance advantage can have a significant impact on productivity.
State of the Bun bundler
The Bun.js bundler is still in beta, so we can expect a few rough edges. But one thing you can't miss is the speed of the engine. Shane O'Sullivan in the SOS blog wrote that building with Bun seemed to “finish as the Enter key was still traveling back upwards from executing the command.” In short, it’s quick. So are the other parts of Bun, like the server and package manager. Again, having all the tools in one place and working fast yields an evolved developer experience, which many developers will welcome.
Take the bundler for a test drive
Let’s take the Bun.js bundler for a test drive with a create-react-app
. For this test, you’ll need Node and/or NPM installed. You can install the create-react-app
with the command: npm i -g create-react-app
, or just use npx
, as we’ll do below.
We of course also need Bun.js, and we’ll need the latest version that includes the bundler (0.6.0+). There are several ways to install bun. You can use: npm: $ npm i -g bun
. Or, if you have it already installed with an older version, make sure to upgrade with $ bun upgrade
.
Let’s create a new application by typing: $ npx create-react-app bun-react
. Then, switch to the new directory with $ cd bun-react
. Note that create-react-app
will run npm install
for you automatically.
If we were just using NPM, we could run the app in dev mode with $ npm run start
. Instead, let’s bundle it together and serve it with Bun.
To start, bundle the application with: $ bun build ./src/index.js --outdir ./build
. That will create a bundle of the application (with a default target of the browser) in the /build
directory.
Now, we need to create an index.html
page like the one shown here.
Listing 1. Index.html with JavaScript and CSS included
<html>
<body>
<link rel="stylesheet" href="App-f6cf171344c59401.css">
<div id="root"></div>
<script type="module" src="/index.js"></script>
</body>
</html>
Note that your CSS file will be different from mine depending on the build output. Once you have the index in place, you can serve the application with: $ bunx serve build
.
Visiting localhost:3000 will now give you the familiar create-react-app
landing page.
Execute the build with a script
You can also use a script file to execute build commands with the Bun API. The one shown in Listing 2 is the equivalent of the command-line call we just executed.
Listing 2. Build script
await Bun.build({
entrypoints: ['./src/index.js'],
outdir: './build',
})
Bundling a server
As the Bun documentation notes, you can often execute server-side code directly; however, bundling such code can reduce startup times. Moreover, the benefits of doing so will compound in CI/CD scenarios.
To bundle a server, you change the target type to "bun
” or “node
,” depending on the runtime you are using. Let’s create a simple Express server and run it with Bun.js.
We’ll use the Hello World server shown in Listing 3.
Listing 3. The Express server
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Next, we use the following script to bundle the server.
Listing 4. Bundling the server
await Bun.build({
entrypoints: ['./index.js'],
outdir: './out',
target: 'bun'
})
This code outputs a complete server to ./out/index.js
. You can run it with bun out/index.js
and get the anticipated behavior (“Hello World!” printed to the browser at localhost:3000
).
Build a standalone executable
The bundler includes a very nice power, to “compile” an application down to an executable target.
We can take our Express server and make an executable with it by using the --compile
switch, as shown in Listing 5.
Listing 5. Compile a standalone server
await Bun.build({
entrypoints: ['./index.js'],
outdir: './out',
target: 'bun'
})
This will drop a server file into the current directory which we can run with: ./server
. When you do this, you’ll get the same server behavior. Pretty slick!
Do more with Bun
Let’s look at a few more of Bun's bundling features. To start, we can have our bundled server (from Listing 3) output a text file to the browser. Create a file in the working directory, named "quote.txt
," and add some text to it. (I used “After silence that which comes nearest to expressing the inexpressible is music.")
We can reference the file in the server as shown in Listing 6.
Listing 6. Outputting a text file to the browser
bun build ./index.js --compile --outfile server
Now we can bundle the text file into our application with the command: $ bun build ./index.js --compile --outfile server
.
When you run the server now, you’ll see the contents of the quote.txt
file output to the browser.
Splitting
Another important feature the Bun.js bundler supports is splitting. In essence, whenever you have two entry points in the application that both rely on the same third bundle, you can instruct the bundler to break this out into a separate chunk. You do this by setting the splitting
property to true
.
We can see this in action by duplicating the index.js
file: $ cp index.js index2.js
. Now, we have two files that both reference the quote.txt
file.
Next, create a build.js
file like the one in Listing 7.
Listing 7. A build file with splitting
const express = require('express')
const app = express()
const port = 3000
import contents from "./quote.txt";
app.get('/', (req, res) => {
res.send(contents)
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
This build file sets splitting
to true
and adds both index.js
and index2.js
to the entrypoints. Now, we can run the build with: $ bun build.js
.
The application will be output to /build
and you’ll see three files: index.js
, index2.js
, and chunk-8e53e696087e358a.js
.
This is a contrived example but in many situations, especially on the front end, splitting is an important feature for reducing code bundle sizes. The browser can cache one version of the bundle and all other code that depends on it will avoid having it included, and also avoid that trip to the server. When conscientiously used, code splitting is key to performance. The Bun bundler makes it dead simple to use.
Plugins
Bun uses a generic plugin format for both the runtime engine and the bundler. You can learn about plugins here.
For the bundler, you can add plugins as shown in Listing 8, where we include a hypothetical YAML plugin.
Listing 8. Plugin syntax
await Bun.build({
entrypoints: ['./index.js','index2.js'],
outdir: './build',
target: 'bun',
splitting: true
})
The bundler also has controls for sourcemap generation, minification, and “external” paths (resources not included in final bundle). You can also customize naming conventions with the naming property and fine-tune the root directory with the root
property. The publicPath
property allows you to define a root path for URLs, to handle things like images being hosted at a CDN.
You can also define global variables that the bundler will replace with the value given by the define
property, which works something like a macro.
Conclusion
Using the Bun.js bundler just feels easy. It takes an overall cleaner approach to bundling than earlier tools, and it applies the lessons learned from them. No doubt, there are edge cases, gotchas, and wrinkles to be ironed out—both in the tool and for complex or very custom build chains. But the Bun.js team is working furiously on the codebase, and we can expect improvements over time. The tool's high points are its simplicity and the fact that it's part of an all-in-one toolset (bundler, packager, runtime), and the palpable speed of the engine. Altogether, you get a distinct feeling that you are using a next-generation tool. It's probably a good indicator of what we can expect to see from server-side JavaScript going forward: comprehensive, seamless, and fast.