What Bun 1.1 Changed: Windows Support, Bundler, Test Runner and Reading Benchmarks
Key takeaways
Bun 1.1 brings Windows support, a unified toolchain (runtime, package manager, bundler, tests), and practical patterns for Next.js, React, Astro, Docker, and CI—with caveats for fast-moving native and framework stacks.
Key takeaways
Compared with 1.0, Bun 1.1 shipped 1,700+ commits and thousands of bug fixes, maturing as an all-in-one JavaScript runtime that combines runtime, package manager, bundler, and test runner in one toolchain. Official support for Windows 10+ greatly widened accessibility, while the speed of bun install, bun run, and bunx, --watch-driven iteration, and stronger Node.js API compatibility are now noticeable in day-to-day work.
This article covers Bun 1.1’s core features (fast runtime, built-in bundler, Jest-compatible testing) and a performance lens vs Node.js and npm (benchmark numbers vary by environment—interpretation matters). It then connects patterns with Next.js, React, and Astro, a small end-to-end sample, and deployment from Docker, CI, and hosting angles.
Caution: Bun moves fast. Before production, run regression tests against your framework, plugins, and native module combinations.
What Bun 1.1 changed: one-line summary
| Area | Highlights in Bun 1.1 |
|---|---|
| Platform | Full support on Windows for runtime, bundler, tests, and package manager |
| Packages | bun install speed, parallel lifecycle scripts, lockfile migration via bun pm migrate |
| Runtime | Large-file transpile cache, Bun Shell (Bun.$), import.meta.env, broader Node-compatible APIs |
| Bundler | Stable --target=node, --compile with NAPI (.node) embedding, built-in modules in macros |
| Tests | Expanded Jest-style matchers, ESM/CJS mocks, runtime in-place mocking |
Core feature ① Ultra-fast runtime and unified tooling
Bun as a runtime
Bun is a JavaScriptCore-based JavaScript runtime that runs TypeScript and JSX with no extra setup. Where Node.js became the de facto server-side standard through npm, core APIs, and ecosystem, Bun aims to cut tool-switching cost by shipping install, run, bundle, and test in a single binary.
Transpiler cache and CLI perceived performance
In Bun 1.1, content-addressed caching for files over 50KB reduces repeated transpile work. As a result, running CLI tools like tsc on Bun has been reported to feel up to roughly 2× faster than before (depends on project size, disk, and CPU).
Bun Shell and cross-platform scripting
Bun 1.1 emphasizes Bun Shell so bash-like syntax stays consistent on Windows. When you run package.json scripts via bun run, the goal is to reduce breakage from cmd vs sh differences.
import { $ } from "bun";
// Pipe to stdout
await $`ls *.ts`;
// Result as string
const listing = await $`ls *.ts`.text();
Why it matters: On Windows, shebangs (#!/usr/bin/env node) are ignored, commands like rm -rf are not available by default, and env var syntax differs. With mixed Windows, macOS, and Linux teams, shell portability is productivity.
Security note: Do not pass raw user input into the shell; follow Bun Shell’s argument escaping patterns.
Core feature ② Built-in bundler (bun build)
Bun’s bundler can emit for browser, Bun, and Node.js targets, handling transpile, bundle, and minification in one pipeline.
Node target: --target=node
Use --target=node when you want a single file that runs on Node.js for libraries or CLIs. Issues such as require-related errors reported in 1.0 were addressed in 1.1, making bundles that include built-ins like node:fs easier to run under Node.
bun build ./src/cli.ts --target=node --outfile=dist/cli.mjs
node dist/cli.mjs
When to use: CLI packages on npm, single-file serverless handlers, or pinning output for legacy Node environments.
Single executable: --compile
--compile bundles TypeScript/JavaScript into a single binary. In Bun 1.1, embedding NAPI addons (.node) makes it easier to ship tools that depend on canvas or native code as one artifact.
bun build --compile ./src/app.ts --outfile=myapp
./myapp
Limits: Outputs can differ by native modules, platform ABI, and shared libraries—plan per-OS build pipelines.
Macros and build-time execution
Bun macros run code at build time and embed results into the bundle. In Bun 1.1, you can import built-ins like node:fs reads and child_process inside macros, simplifying build-time code generation.
import { readFileSync } from "node:fs" with { type: "macro" };
export const buildTimeText = readFileSync("./version.txt", "utf8");
Production tip: Macros can break reproducible builds. In CI, pin inputs—keep read files in the repo under version control.
Core feature ③ Test runner (bun test)
Bun tests expose a Jest-like API (describe, test, expect) and run TypeScript tests with minimal configuration.
Richer matchers
Bun 1.1 greatly expanded expect matchers—partial object matching, whitespace-insensitive comparison, Promise result matching—suited to codebases that prefer explicit assertions over snapshots.
import { expect, test } from "bun:test";
test("partial object match", () => {
expect({ a: { b: 1, c: 2 } }).toEqual({
a: expect.objectContaining({ b: 1 }),
});
});
Module mocks and ESM/CJS
Bun stresses mocking both ESM and CommonJS, including in-place updates for already-imported modules at runtime—often easing Jest-style hoisting / mock order pain.
import { mock, test, expect } from "bun:test";
import { fn } from "./math";
test("local module mock", () => {
mock.module("./math", () => ({ fn: () => 42 }));
expect(fn()).toBe(42);
});
Caution: Mocking can leave global test pollution. Pair with per-file isolation strategies (e.g. --preload reset hooks).
--watch and the dev loop
bun test --watch reruns quickly on file changes. The Bun 1.1 announcement also highlights Windows optimizations to cut save-to-rerun latency. In large monorepos, narrow test scope to directories when possible.
Performance vs Node.js and npm: how to read numbers
Official Windows-oriented examples
Bun 1.1 materials cite a Vite React template on Windows where bun install was up to ~18× faster than yarn and ~30× faster than npm in their scenario; bun run vs npm run up to ~11×, and bunx vs npx up to ~11×.
How to interpret:
- Ratios swing with cache state, whether
node_moduleswas removed, network, disk, and antivirus. - Production runtime performance and install speed are different problems—the former depends on app code, I/O, and GC.
- Compatibility trumps speed—validate against core dependencies.
.bunx and execution paths on Windows
Windows often degrades scripting via symlinks, shebangs, and batch prompts (“Terminate batch job?”). Bun 1.1 introduced .bunx to mitigate this. Even if you use Bun only as a package manager, generated executables are intended to work alongside Node—important in practice.
Filesystem API micro-benchmarks
The same API can differ in speed by implementation—for example, fs.readdir() on Windows has shown meaningful gains vs Node in Bun’s benchmarks. For APIs not yet optimized, track issues and plan workarounds.
Measuring startup and runtime on your own code
Third-party “Bun vs Node” tables tend to measure a hello-world server or console.log. Those numbers are real, but they tell you little about an app that spends its time in database calls, JSON serialization, and framework middleware. Measure the three things that actually differ, on your own project:
# 1. Process startup (matters for CLIs, serverless cold starts, test files)
hyperfine --warmup 3 'node dist/cli.js --version' 'bun src/cli.ts --version'
# 2. Test suite wall time (same tests, both runners)
hyperfine --runs 5 'npx jest' 'bun test'
# 3. Server throughput on a real endpoint, not a hello-world route
npx autocannon -c 50 -d 30 http://localhost:3000/api/orders
The pattern you should expect: startup and TypeScript execution show the largest gaps, because Bun transpiles TypeScript natively and starts fast, while Node needs a compile step or a loader such as tsx. Install and test time follow. Request throughput on an I/O-bound endpoint shows the smallest difference, because the database round trip dominates both runtimes. If a migration pitch rests on HTTP benchmark numbers, re-run them against your slowest real endpoint before believing them.
Integration with Next.js, React, and Astro
Common baseline: Bun as package manager
The safest first step in most frontend projects is replacing npm/pnpm installs with bun install and running scripts with bun run. If you have package-lock.json, bun pm migrate (or Bun’s automatic migration) can produce bun.lockb.
bun install
bun run dev
React (e.g. Vite / CRA-style)
React itself does not require Node at runtime; the build chain matters. With Vite + React, Bun usually accelerates installs, script execution, and sometimes tests (bun test). Whether to replace the bundler entirely depends on team standards and plugin compatibility.
Next.js: separate runtime from bundler
Next.js owns its build pipeline (Turbopack/Webpack, etc.). A common pattern is Bun for installs and the dev server process while leaving Next’s bundler unchanged. Some platforms (e.g. Vercel) offer Bun as a function runtime option.
For local Bun + Next, project-specific scripts may look like this (adjust to your template and Next version):
{
"scripts": {
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
}
}
Recommendation: Next releases quickly and combines native modules, Server Components, and edge runtimes—validate staging E2E and critical API paths.
Astro (SSG, edge-friendly)
Astro focuses on static output and islands to shrink bundles. In this blog’s stack (Astro + Cloudflare Pages), Bun fits accelerating the dev loop.
bun create astro@latest my-astro-site
cd my-astro-site
bun install
bun run dev
Cloudflare adapter, image optimization, MDX, and other plugin combos may still assume Node—track issues if something breaks.
Hands-on sample: mini API + tests + single-binary deploy
Below is a minimal flow: Bun HTTP server, unit tests, and compiled output.
Project layout
mini-bun-service/
├─ package.json
├─ src/
│ ├─ server.ts
│ └─ server.test.ts
└─ README.md
package.json
{
"name": "mini-bun-service",
"type": "module",
"scripts": {
"dev": "bun --watch src/server.ts",
"start": "bun src/server.ts",
"test": "bun test",
"build:bin": "bun build --compile src/server.ts --outfile=dist/server"
}
}
bun --watch restarts the server on changes for a faster local iteration loop.
src/server.ts
const port = Number(process.env.PORT ?? 3000);
export function createServer() {
return Bun.serve({
port,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/health") {
return Response.json({ ok: true, bun: process.versions.bun ?? "dev" });
}
return new Response("Not Found", { status: 404 });
},
});
}
if (import.meta.main) {
const server = createServer();
console.log(`listening on ${server.url}`);
}
Note: Bun.serve stands up a dev API server with very little code. In production, you typically add a reverse proxy, logging, rate limits, and security headers.
src/server.test.ts
import { describe, test, expect } from "bun:test";
import { createServer } from "./server.ts";
describe("/health", () => {
test("returns 200 and JSON body", async () => {
const server = createServer();
try {
const res = await fetch(`${server.url}health`);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.ok).toBe(true);
} finally {
server.stop();
}
});
});
Build and run
bun test
bun run build:bin
./dist/server
Extensions: env validation (e.g. Zod), structured logging, OpenTelemetry, aligning /health with load balancer health checks.
Deployment strategy
Containers (Docker)
Prefer official Bun images or multi-stage builds: install deps → test → copy --compile artifact. Slim runtime images help cold start and attack surface.
# Example: run tests in the build stage
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock* bun.lockb* ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun test && bun build --compile src/server.ts --outfile /out/server
FROM gcr.io/distroless/base-debian12
COPY --from=build /out/server /server
ENV PORT=8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
Note: Match base image and libc when embedding .node binaries.
Static and hybrid frontends (Astro, Next export, etc.)
- Static output (Astro
dist/) goes to static hosting (Cloudflare Pages, Netlify, S3+CloudFront). Bun speeds the build pipeline. - For server features, set edge/serverless targets per adapter and confirm in platform docs whether the runtime is Bun or Node.
CI (GitHub Actions, etc.)
Include the lockfile in cache keys and use bun install --frozen-lockfile for reproducible installs. Bun 1.1 writes a binary bun.lockb; Bun 1.2 switched the default to a text-based bun.lock that diffs cleanly in code review. If you upgrade, commit the new lockfile and update Docker COPY lines and CI cache keys to match (COPY package.json bun.lock* bun.lockb* ./ covers both). Add quality gates with bun test --coverage (as configured).
# Conceptual example—tune to your repo
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun test
Rollback and observability
Tag releases clearly so you can roll back to the previous binary or assets quickly, and dashboard error rate, latency, and CPU. Switching to Bun does not simplify your application’s algorithmic complexity.
Where Bun still lags behind Node.js
Speed isn’t the question that decides most migrations. Compatibility and tooling are. These are the areas to check before moving a production service:
| Area | Situation | What to do |
|---|---|---|
| Native addons | Bun implements Node-API, so many N-API addons load. Addons written against V8 or NAN APIs directly do not | Check each node-gyp dependency; prefer N-API builds or Bun-native alternatives (bun:sqlite instead of better-sqlite3) |
node:cluster | Support arrived later and is more limited than Node’s | On Linux, run several processes with Bun.serve({ reusePort: true }), or scale with containers/process managers instead |
| V8-specific tooling | Bun runs JavaScriptCore, so tools that hook V8 internals (--prof, clinic.js, V8 heap snapshot workflows) don’t apply | Use bun --inspect with Bun’s web debugger or the VS Code extension; keep a Node build for deep V8 profiling if you depend on it |
| Edge-case Node APIs | Most of fs, path, crypto, http, events, worker_threads work; less common modules and flags can have gaps | Check Bun’s Node.js compatibility page for the modules you actually import |
| Ecosystem maturity | Middleware, APMs and hosting platforms are tested primarily on Node | Verify your APM/tracing agent and host explicitly support Bun |
Most of these show up as a failure at startup or on the first request, which is the good case. The harder ones are behavioral differences — date parsing, Intl output, timing of stream events — that pass unit tests and diverge under real traffic. Run a production-shaped load test and diff responses against the Node deployment before switching traffic.
Migrating an existing Node.js project
Command mapping
npm install → bun install
npm run dev → bun run dev
npx some-cli → bunx some-cli
node index.js → bun index.js
npx tsx script.ts → bun script.ts # TypeScript runs directly
Bun’s transpiler strips types but does not type-check. Keep tsc --noEmit in CI, or type errors that Node’s build step used to catch will reach production.
Things Bun does implicitly
.envfiles are loaded automatically (.env,.env.local,.env.production, etc.), sodotenvbecomes redundant. It also means a stray.envin the working directory now changes behavior. In containers, rely on real environment variables and keep.envfiles out of the image.- Bun-specific APIs such as
Bun.file(),Bun.write(),Bun.serve()andbun:sqliteare faster conveniences, but every call ties that module to Bun. Keep them behind a small adapter if there’s any chance you’ll need to run the same code on Node.
// Bun-only file helpers
const config = await Bun.file('./config.json').json();
await Bun.write('./out/report.txt', 'done');
// Portable equivalent that runs on both
import { readFile, writeFile } from 'node:fs/promises';
const config2 = JSON.parse(await readFile('./config.json', 'utf8'));
await writeFile('./out/report.txt', 'done');
Tests from Jest
bun test understands describe/test/expect and provides Jest-style mocks, so many suites run unchanged. The usual friction points are Jest config features (moduleNameMapper, custom environments like jsdom, transform settings) and mocks that rely on Jest’s hoisting of jest.mock calls.
import { expect, test, mock } from "bun:test";
test("mock function", () => {
const fetchUser = mock(() => ({ id: 1 }));
fetchUser();
expect(fetchUser).toHaveBeenCalledTimes(1);
});
The hybrid approach: Bun for development, Node in production
The lowest-risk adoption path is to use Bun where it’s fast and easy to undo — installs, scripts, the dev loop, tests — and keep Node as the production runtime until compatibility is proven:
{
"scripts": {
"dev": "bun --watch src/index.ts",
"test": "bun test",
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js"
}
}
The catch is that the code now has to run on both runtimes. A Bun.file() call that works in bun --watch throws ReferenceError: Bun is not defined under node dist/index.js. Add a lint rule that forbids the Bun global and bun:* imports in application code, and run at least a smoke test with node in CI. That way the first time the Node path executes isn’t the production deploy.
Troubleshooting checklist
- Native modules:
better-sqlite3,sharp, etc. tie to Node ABI—evaluate Bun-native options (bun:sqlite, etc.) or keep those paths on Node. - Next / bundler plugins: Experimental combos can be flaky—build a minimal repro repo when debugging.
- Dates, regex, engine differences: Bun uses JavaScriptCore; subtle differences vs V8 may remain. Bun 1.1 improved areas like
Date.parse, but time zones and formats still need tests. - Windows paths: Path-related bugs land across versions—always use
path.join-style normalization.
Related Articles
- Bun Shell complete guide — cross-platform scripting
- Jest in Practice: Matchers, Async Tests, Mocks, Snapshots and Coverage in CI