Bun 1.4 Absorbs Image Processing, Browser Automation, and Cron Into the Runtime
Bun v1.4 Update Summary: Packing Browsers, Images, Scheduled Tasks, and Engineering Tools into One Runtime
When we develop JavaScript projects, we usually install sharp for image processing, Playwright for browser automation, node-cron for scheduled tasks, and node-pty for terminal interaction.
Bun 1.4 continues to do one thing: put these common capabilities directly into the Bun binary.
Official update article: https://bun.sh/blog/bun-v1.4.
Let's start with the conclusion.
The most noteworthy thing about Bun 1.4 is not a single API, but that it is starting to look more like a complete JavaScript toolbox.
This update covers the runtime, HTTP service, package manager, testing, builder, performance, security, and platform support.
Small dependencies like configuration parsing, log formatting, and compression are also starting to have built-in alternatives.
Some features in the article already appeared in Bun 1.3.x, and Bun 1.4 continues to enhance them.
Therefore, the article will mark 1.4 New and 1.4 Enhanced to avoid mixing up the version timeline.
Feature Overview
| Category | Feature | My Judgment |
|---|---|---|
| Runtime | Bun.Image |
Useful, suitable for image uploads and thumbnails |
| Runtime | Bun.WebView |
Interesting and useful, suitable for browser automation |
| Runtime | Bun.markdown |
Useful, suitable for Markdown rendering |
| Runtime | Bun.cron |
Useful, suitable for scheduled tasks |
| Runtime | Bun.Terminal |
Useful, suitable for terminal and PTY tools |
| Runtime | JSON5, JSONL, JSONC, XML, TOML | Useful, no need to install packages for config and log parsing |
| Runtime | Compression streams, textStream(), Bun.Archive |
Useful, streaming compression and tar handling |
| Runtime | Bun.serve() static directory |
Important, reduces web service dependencies |
| HTTP | Request compression | Useful, suitable for large JSON uploads |
| HTTP | Proxy authentication and TLS session reuse | Useful, intranet proxy and high-frequency connections |
| HTTP | Streaming response backpressure | Important, slow clients no longer drain memory |
| HTTP | HTTP/2 and HTTP/3 | Interesting, currently experimental features |
| Engineering | bun run --parallel |
Useful, suitable for Monorepo |
| Engineering | Global virtual store | Important, suitable for CI and multi-project development |
| Engineering | bun pm diff |
Useful, check differences before upgrading dependencies |
| Engineering | bun update and nested overrides |
Useful, more granular dependency governance |
| Testing | Parallel, isolation, sharding, affected tests | Important, suitable for large test suites |
| Build | React Compiler | Important, reduces manual optimization |
| Build | bun:bundle feature flags |
Useful, remove dead code at build time |
| Build | In-memory files and single-file HTML | Interesting, suitable for code generation and offline distribution |
| Build | ESM bytecode and faster code splitting | Useful, more complete single-file distribution |
| Performance | Rust rewrite and JavaScriptCore optimization | Important, production resource consumption drops |
| Production | memoryPressure low memory notification |
Useful, proactively release memory before OOM |
| Security | TLS, HTTP parsing, and tar package hardening | Important, recommend all projects upgrade |
| Platform | Windows ARM64, FreeBSD, experimental Android | Useful, wider platform coverage |
Start Upgrading
First, upgrade to Bun 1.4.
bun upgrade
bun --version
Then reinstall dependencies in the project root.
bun install
If the project uses native extensions, Monorepo, or custom TLS, it is recommended to read the upgrade notes at the end of the article first.
Bun.Image: Built-in Image Processing
Version: Provided in 1.3.14, included and further improved in 1.4
Bun.Image is Bun's built-in image processing library.
It can decode, scale, rotate, and re-encode, supporting JPEG, PNG, WebP, GIF, and BMP.
On macOS and Windows, HEIC, AVIF, and TIFF are also available.
Previously, processing thumbnails usually required installing sharp and the corresponding native addon.
Now it can be written directly in a Bun project.
await Bun.file("photo.jpg").image().resize(1024, 1024, { fit: "inside" }).rotate(90).webp({ quality: 85 }).write("thumb.webp");
This code reads photo.jpg, constrains the image within 1024×1024, rotates it 90 degrees, and outputs it as a quality 85 WebP.
When processing uploaded files, it can also directly return an HTTP response.
Bun.serve({
async fetch(req) {
const form = await req.formData();
const upload = form.get("file");
if (!(upload instanceof File)) {
return new Response("file is required", { status: 400 });
}
const image = new Bun.Image(upload).resize(200).jpeg();
return new Response(image, {
headers: { "Content-Type": "image/jpeg" }
});
}
});
The benchmark given in the official article is that when scaling a 1080p PNG to a 400×400 JPEG, Bun.Image is 1.38 times faster than sharp.
This number is an official benchmark and does not mean all images and machines will get the same result.
Bun.WebView: Drive Browsers Without Playwright
Version: Provided in 1.3.12, enhanced in 1.4
Bun.WebView is a built-in headless browser automation API.
It can open web pages, click elements, execute JavaScript, take screenshots, and send CDP commands.
The most interesting part is that clicks and scrolls are real user inputs, and the page can observe event.isTrusted === true.
await using view = new Bun.WebView({ width: 800, height: 600 });
await view.navigate("https://bun.sh");
await view.click("a[href='/docs']");
const title = await view.evaluate("document.title");
console.log(title);
await Bun.write("page.png", await view.screenshot());
macOS uses the system WebKit by default.
macOS, Linux, and Windows can also drive an already installed Chrome, Chromium, or Edge.
Therefore, when using a Chrome path on Windows and Linux, the corresponding browser needs to be installed on the machine first.
It is suitable for web page screenshots, background inspections, page operations after automated login, and simple end-to-end scripts.
Bun.markdown: Markdown Parser Ready to Use
Version: Provided in 1.3.8, enhanced in 1.4
Bun.markdown can convert Markdown into HTML, React elements, or terminal ANSI text.
The simplest usage is as follows.
const markdown = "# Hello **world**";
const html = Bun.markdown.html(markdown);
console.log(html);
// <h1>Hello <strong>world</strong></h1>
In a React page, it can directly return React elements.
export default function ReadmePage({ readme }: { readme: string }) {
return Bun.markdown.react(readme);
}
It supports GFM tables, strikethrough, task lists, and autolinks.
It also supports custom renderers, for example, turning headings into terminal bold and underline.
const output = Bun.markdown.render("# Hello\n\n**bold**", {
heading: (children) => `\x1b[1;4m${children}\x1b[0m\n`,
paragraph: (children) => `${children}\n`,
strong: (children) => `\x1b[1m${children}\x1b[22m`
});
process.stdout.write(output);
There is a security boundary that must be remembered.
Bun.markdown.html() does not automatically sanitize HTML.
Raw HTML, event attributes, and javascript: links may be output as-is.
Therefore, when rendering untrusted user content, you must first use an HTML sanitizer before putting the result into a web page.
Bun.cron: Hand Scheduled Tasks to the Operating System
Version: Provided in 1.3.11, enhanced in 1.4
Bun.cron can register OS-level scheduled tasks.
Linux uses crontab, macOS uses launchd, and Windows uses Task Scheduler.
await Bun.cron("./worker.ts", "30 2 * * MON", "weekly-report");
worker.ts can export a scheduled handler.
export default {
async scheduled(controller: { cron: string; scheduledTime: number }) {
console.log(controller.cron, controller.scheduledTime);
await generateWeeklyReport();
}
};
async function generateWeeklyReport() {
console.log("report generated");
}
If you only want to run it within the current Bun process, you can also pass a function.
using job = Bun.cron("*/5 * * * *", async () => {
await cleanupTempFiles();
});
job.unref();
The file form depends on the OS's task scheduling permissions.
The function form only runs in the current event loop and does not register a system task.
Bun 1.4 uses local time by default and adds the { tz } option to specify a timezone.
When deploying across regions, it is recommended to explicitly write the timezone and not rely on the server's default timezone.
Bun.Terminal: Built-in Pseudo-Terminal
Version: Provided in 1.3.5, enhanced in 1.4
Bun.Terminal is a built-in PTY, a pseudo-terminal that can run interactive commands.
It can drive bash, vim, or htop without needing to install node-pty.
const proc = Bun.spawn(["bash"], {
terminal: {
cols: 80,
rows: 24,
data(_terminal, data) {
process.stdout.write(data);
}
}
});
proc.terminal.write("echo Hello from PTY!\n");
Linux, macOS, and Windows all support this API.
The example uses bash; on Windows, you need to replace it with the shell that actually exists on the machine.
It is suitable for terminal panels, remote command execution interfaces, and interactive CLI wrappers.
Built-in Parsers: JSON5, JSONL, JSONC, XML, and TOML
Version: Gradually built-in since 1.3.x, further enhanced in 1.4
Configuration file and log parsing are another batch of frequently installed dependencies.
Bun has also made them built-in APIs.
Bun.JSON5
Bun.JSON5 is used to parse and generate JSON5, the kind of JSON that allows comments, trailing commas, and unquoted keys.
const config = Bun.JSON5.parse(`{
// Database configuration
host: "localhost",
port: 3306,
}`);
console.log(config.port);
// 3306
.json5 files can also be imported directly.
import config from "./config.json5";
It replaces the json5 package.
Bun.JSONL
Bun.JSONL is used to process line-delimited JSON, where each line is a JSON object.
Log files and large-scale data exports often use it.
const events = Bun.JSONL.parse('{"type":"login"}\n{"type":"logout"}\n');
console.log(events.length);
// 2
When processing streaming data, you can use parseChunk() to parse chunk by chunk without waiting for the entire file to be read.
It replaces ndjson.
Bun.JSONC
Bun.JSONC.parse() parses JSON with comments and trailing commas, the format used by tsconfig.json.
const tsconfig = Bun.JSONC.parse(`{
"compilerOptions": {
"strict": true, // Strict mode
},
}`);
console.log(tsconfig.compilerOptions.strict);
// true
It replaces jsonc-parser.
Bun.XML
Bun.XML is a SIMD-accelerated XML parser and serializer.
.xml files can be imported directly.
import sitemap from "./sitemap.xml";
Note a behavior change in 1.4: .xml imports now return the parsed object, not the file path.
When a path is needed, explicitly use --loader .xml:file instead.
It replaces fast-xml-parser and xml2js.
Bun.TOML
Bun.TOML supports TOML v1.1.0, passing all 708 test cases of toml-test.
1.4 adds stringify(), which can serialize an object back to TOML.
const toml = Bun.TOML.stringify({
name: "my-app",
scripts: { dev: "bun run dev.ts" }
});
console.log(toml);
It replaces @iarna/toml.
Compression Streams, textStream(), and Bun.Archive
Version: Gradually built-in since 1.3.x, further enhanced in 1.4
CompressionStream and DecompressionStream
Web standard compression streams can be used directly in Bun, supporting gzip, deflate, deflate-raw, brotli, and zstd.
const compressed = new Blob(["hello world".repeat(1000)]).stream().pipeThrough(new CompressionStream("gzip"));
await Bun.write("data.gz", compressed);
Decompression uses DecompressionStream in reverse.
It is suitable for streaming compression and decompression of data, which is different from the compress option of fetch().
Response.textStream()
textStream() returns a string stream decoded as UTF-8, so you don't need to assemble a TextDecoder yourself when reading large text responses.
const response = await fetch("https://example.com/big.txt");
for await (const chunk of response.textStream()) {
process.stdout.write(chunk);
}
Bun.Archive
Bun.Archive is used to create and extract tar packages, and the entire process runs off the main thread without blocking the event loop.
For scenarios like build artifact packaging and cache archiving, you no longer need to install the tar dependency.
For the specific API, see the Archive section of the official documentation.
bun run --parallel: Run Multiple Scripts in Parallel
Version: Provided in 1.3.9, enhanced in 1.4
Previously, you often had to install concurrently or npm-run-all to run scripts in parallel.
Bun 1.4 allows you to write it directly like this.
bun run --parallel build test
You can also use script name wildcards.
bun run --parallel "build:*"
In a Monorepo, you can make all workspaces execute build simultaneously.
bun run --parallel --filter '*' build
Continue executing other tasks even if one workspace fails.
bun run --parallel --no-exit-on-error --filter '*' test
The output will automatically include the script name, making it easier to locate the source when multiple tasks output simultaneously.
Note that if scripts share ports, temporary directories, or databases, you still need to handle resource conflicts yourself.
Bun.serve(): Serve Static Directories Directly
Version: New in 1.4
Routes in Bun.serve() can now directly map a directory.
Bun.serve({
port: 3000,
routes: {
"/static/*": { dir: "./public" }
}
});
public/index.html will be returned as the directory index.
Bun will also automatically handle Content-Type, ETag, Last-Modified, 304, and Range.
This means many projects no longer need to install express.static, serve-static, or sirv separately.
Video and large file downloads can also directly utilize byte range requests.
Bun.serve({
port: 3000,
routes: {
"/video.mp4": new Response(Bun.file("./video.mp4"))
}
});
Test range requests.
curl -H "Range: bytes=0-1023" http://localhost:3000/video.mp4
Under normal circumstances, it should return 206 Partial Content.
Bun will normalize static file paths, and on Linux, it will also use openat2 to restrict symlinks from escaping the target directory.
Incidentally, the backpressure handling for streaming responses (new in 1.4).
When the client receives data very slowly and the socket send buffer is full, Bun will pause the stream's pull() and wait for the buffer to drain before continuing.
In Bun 1.3, the behavior was to keep piling unsent data into memory; if the slow client persisted long enough, the process would be dragged down.
After 1.4, serving large files and streaming interfaces to slow clients is much more stable.
fetch() Request Compression: Save Traffic When Uploading Large JSON
Version: New in 1.4
fetch() adds a compress option, which can compress the request body before sending the request.
Supports gzip, deflate, br, and zstd.
const response = await fetch("https://api.example.com/upload", {
method: "POST",
body: JSON.stringify({ items: Array(10_000).fill({ ok: true }) }),
compress: { encoding: "gzip", level: 6 }
});
console.log(response.status);
Bun will automatically set Content-Encoding and make Content-Length reflect the compressed size.
Buffered bodies like strings, ArrayBuffer, TypedArray, and Blob will be automatically compressed.
Streaming request bodies will be sent as-is without automatic compression.
The server must support the corresponding Content-Encoding.
The proxy option for fetch() has also been enhanced (provided in 1.3.4).
proxy can now accept an object, allowing you to send custom request headers directly to the proxy server, such as Proxy-Authorization.
await fetch("https://api.example.com/data", {
proxy: {
url: "http://proxy.example.com:8080",
headers: { "Proxy-Authorization": "Bearer token" }
}
});
For scenarios involving authenticated proxies on a corporate intranet, where you previously had to assemble headers yourself, now a single object suffices.
TLS session reuse is also a new change in 1.4.
For a second cold connection to the same origin, a cached session can be reused, restoring the connection in 1 round trip without going through the full handshake and certificate chain verification again.
For services with high-frequency short connections, such as tasks that periodically call external APIs, the connection overhead will be significantly reduced.
HTTP/2 and HTTP/3: Interesting, But Don't Rush to Production Yet
Version: Provided in 1.3.14, enhanced in 1.4
fetch() can explicitly specify HTTP/2 or HTTP/3.
const response = await fetch("https://example.com", {
protocol: "http3"
});
console.log(response.status);
Under HTTP/2, concurrent requests to the same origin can reuse a single connection.
Bun.serve() can also enable HTTP/3.
Bun.serve({
port: 443,
tls: {
cert: Bun.file("./cert.pem"),
key: Bun.file("./key.pem")
},
http3: true,
fetch() {
return new Response("hello");
}
});
HTTP/3 will additionally listen on the same port over UDP.
The official documentation explicitly marks these two capabilities as experimental features.
Bun.serve()'s HTTP/3 does not yet support full 0-RTT recovery, and server.upgrade() on HTTP/3 will also return false.
Therefore, it can be used for experiments and stress testing, but it is not recommended as a default production configuration right now.
bun:ffi: Faster Native Function Calls
Version: New engine-level implementation in 1.4
bun:ffi uses the built-in FFI capability of JavaScriptCore to call C functions.
In official benchmarks, some empty calls are about 3 times faster, and CString scenarios are about 3.8 times faster.
Below is an example of the API shape.
import { dlopen } from "bun:ffi";
const { symbols } = dlopen("libhash.so", {
hash: {
args: ["buffer", "buffer_length"],
returns: "cstring"
}
});
const data = new Uint8Array([1, 2, 3]);
const digest = symbols.hash(data, data);
console.log(digest);
libhash.so and hash are just example placeholders.
In a real runtime, you need to prepare a dynamic library that matches the current operating system, architecture, and ABI.
In Bun 1.4, returns: "cstring" returns a normal string, and a C NULL pointer returns null.
If you use CString to manage memory, you need to keep the original pointer, as CString no longer provides .ptr.
Global Virtual Store: Fewer Copies When Reinstalling Dependencies
Version: Provided in 1.3.14, included and further optimized in 1.4
The global virtual store is only enabled when the isolated linker is selected.
Dependency packages are extracted only once to Bun's global cache, and then placed into each project's node_modules/.bun/ directory via symlinks.
This is an opt-in choice; existing projects will not automatically switch after upgrading Bun.
bun install --linker=isolated
You can also fix the configuration in bunfig.toml.
[install]
linker = "isolated"
It is suitable for scenarios where there are many projects on the local machine, or where the CI cache is relatively stable.
The official article claims that in CI scenarios where the lockfile exists and the cache is pre-warmed, installing 1400 packages can be up to 7 times faster.
Newly created Monorepos use the isolated linker by default.
Existing lockfiles will not automatically change the directory layout just because Bun is upgraded.
bun pm diff: Check Code Differences Before Upgrading Dependencies
Version: New in 1.4
Before upgrading dependencies, the biggest fear is not knowing what exactly the new version changed.
bun pm diff compares two package versions and prompts file changes, install scripts, and dangerous Node.js module imports.
bun pm diff react
bun pm diff react@18.2.0 19.0.0
bun pm diff ./vendored-pkg pkg@2.1.0
You can also only look at matching files.
bun pm diff react-dom@18.2.0 18.3.1 "*.min.js"
It is suitable as a manual check step before upgrading dependencies.
bun audit fix: Fix Dependency Vulnerabilities
Version: New in 1.4
bun audit fix
This command attempts to upgrade vulnerable dependencies and reinstall them.
If you need to cross major versions, you can use --latest.
bun audit fix --latest
bun audit fix --dry-run
--dry-run only shows what is planned to be modified without immediately writing to dependencies.
If the upstream dependency's version range prevents the upgrade, Bun may only be able to fix some vulnerabilities.
Therefore, a successful command execution does not mean all vulnerabilities have been resolved; you still need to check the final report.
bun dedupe, bun prune, and License List
Version: New in 1.4
bun dedupe merges duplicate dependencies that satisfy the same semver range.
bun dedupe
bun dedupe --check
--check is suitable for CI, causing the check to fail when deduplicable dependencies are found.
bun prune is used to delete dependencies that do not exist in the lockfile.
bun prune
bun prune --production
A typical container flow is as follows.
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
RUN bun prune --production
This way, the build stage retains dev dependencies, and the published image only leaves production dependencies.
View license information for production dependencies.
bun pm licenses --prod --json > licenses.json
Workspace filter and catalog
Version: New or enhanced in 1.4
You can add a dependency to only a specific workspace.
bun add zod --filter api
You can also run a specific workspace and its dependencies.
bun run --filter 'web...' build
web... means web and the workspaces it depends on.
...web means the workspaces that depend on web.
catalog is used to uniformly manage common dependency versions in a Monorepo.
bun add react --catalog
The workspace's package.json can reference the catalog version.
{
"dependencies": {
"react": "catalog:"
}
}
This way, when upgrading React, you only need to maintain a single version number.
bun update and Nested overrides
Version: New in 1.4
bun update now also updates transitive dependencies
Previously, bun update only updated dependencies directly declared in package.json.
After 1.4, dependencies of dependencies will also be updated together.
bun update
When specifying a package name, it will update all copies of this package in the entire dependency tree, including copies in other workspaces.
bun update zod
bun update '@types/*' --latest
You can also batch update by pattern, for example, updating all type packages to the latest version at once.
One small change to note: when the updated package is not depended on anywhere currently, Bun will now error and exit instead of adding it as a new dependency.
Nested overrides
Previously, if you wanted to override a dependency of a dependency, you could only do a global replacement, affecting the entire dependency tree.
1.4 supports nested overrides, only touching the copy under the specified parent dependency.
{
"overrides": {
"express": {
"qs": "6.13.0"
},
"lodash@<4.17.21": "4.17.21"
}
}
This configuration means only pinning qs inside express to 6.13.0; qs used by other packages is not affected.
lodash@<4.17.21 means only overriding lodash versions lower than this one.
npm's nested syntax, Yarn's a/b syntax, and pnpm's a>b syntax are all supported.
Note that after using nested or version-qualified overrides, the lockfile will become lockfileVersion: 3, which older versions of Bun cannot read.
Testing Capabilities: Parallel, Isolation, Sharding, and Running Only Affected Tests
bun test --parallel
Version: Provided in 1.3.13, enhanced in 1.4
bun test --parallel
bun test --parallel=4 --isolate
Bun will start multiple workers and assign test files to idle workers.
Coverage and JUnit reports will be merged.
When testing in parallel, pay attention to resource contention for ports, databases, and temporary directories.
bun test --isolate
Version: Provided in 1.3.13, enhanced in 1.4
--isolate makes each test file run in a brand new JavaScript global, but still within the same process.
This is consistent with the default behavior of Jest and Vitest.
It specifically solves a classic problem: a single file passes, but the entire suite fails when run together.
bun test --isolate
Between two files, Bun will do the following cleanup.
- Create a new
globalThis; properties and patches attached by the previous file all disappear. - Clear the ESM and CommonJS module registries; each file re-executes its own imports.
- Close servers, sockets, file watchers, and child processes left by the previous file; cancel timers.
- Re-execute
--preloadscripts.
Transpiled artifacts and bytecode are cached at the process level. When the second file imports the same module, it only needs to re-execute the module's top-level code without re-reading and transpiling the file.
--parallel implicitly defaults to --isolate; if you don't want isolation, you can turn it off with --no-isolate.
bun test --shard
Version: Provided in 1.3.13, enhanced in 1.4
When CI has multiple runners, you can split tests into multiple shards.
bun test --shard=1/3
bun test --shard=2/3
bun test --shard=3/3
Bun will first sort the test files, then distribute them to each shard in a round-robin manner.
It can also be combined with --parallel, allowing each CI machine to continue using multiple workers.
--timings
Version: New in 1.4
First, record the time taken for each test file.
bun test --timings=timings.json --update-timings
The next time you shard, Bun will try to balance based on time taken rather than file count.
bun test --shard=1/3 --timings=timings.json
The first run has no historical timings; subsequent runs will become increasingly accurate.
--changed
Version: Provided in 1.3.13, enhanced in 1.4
bun test --changed
bun test --changed=main
Bun will check Git changes and then trace back along import relationships to find affected tests.
This is more practical than just running tests corresponding to changed files.
Dependencies that are dynamically imported or cannot be analyzed by tools may not be fully captured.
retry and repeats
Version: Provided in 1.3.3, enhanced in 1.4
For tests that fail intermittently, you can set a limited number of retries.
test(
"unstable service",
async () => {
expect(await checkService()).toBe(true);
},
{ retry: 5 }
);
To verify test stability, you can force repeated execution.
test(
"stress check",
() => {
expect(runCheck()).toBe(true);
},
{ repeats: 20 }
);
You can also set a default retry count for the entire test suite.
bun test --retry 5
Retries only reduce the impact of intermittent failures on CI; they are not a substitute for fixing flaky tests.
Build Capabilities: React Compiler and Build-time Feature Flags
Built-in React Compiler
Version: New in 1.4
Bun can automatically optimize React components and Hooks at build time.
bun build ./src/index.tsx --outdir ./dist --react-compiler
The API approach is as follows.
await Bun.build({
entrypoints: ["./src/index.tsx"],
outdir: "./dist",
reactCompiler: true
});
In official benchmarks, enabling the Compiler for a large React project only adds about 71 milliseconds of build time.
This result is from the official test project and should not be taken as a fixed time for all projects.
Barrel import optimization
Version: Provided in 1.3.10, enhanced in 1.4
When importing barrel packages like antd, Bun will try to skip modules that are not used.
import { Button } from "antd";
If the package correctly declares sideEffects: false, the optimization can be enabled automatically.
You can also configure it explicitly.
await Bun.build({
entrypoints: ["./src/index.tsx"],
outdir: "./dist",
optimizeImports: ["antd", "@mui/material"]
});
You should only set sideEffects: false when you are sure the package has no import side effects.
bun:bundle: Build-time Feature Flags
Version: Provided in 1.3.5, further improved in 1.4
bun:bundle can decide whether a piece of code exists at build time.
import { feature } from "bun:bundle";
if (feature("SUPER_SECRET")) {
console.log("secret feature enabled");
}
Pass the feature name at build time.
bun build --feature=SUPER_SECRET ./src/index.ts
Branches that are not enabled will be removed as dead code.
It is a build-time switch, not a runtime environment variable.
In-memory Files, Single-file HTML, and Asset Embedding
Bun.build() using in-memory files
Version: Provided in 1.3.6, further improved in 1.4
Code generators or test tools don't necessarily need to write files to disk first.
const result = await Bun.build({
entrypoints: ["/app/index.ts"],
files: {
"/app/index.ts": `import { greet } from "./greet.ts";
console.log(greet("World"));`,
"/app/greet.ts": `export function greet(name: string) {
return "Hello, " + name + "!";
}`
}
});
console.log(result.success);
The value of files can be a string, Blob, or TypedArray.
Single-file HTML
Version: Provided in 1.3.10, enhanced in 1.4
bun build ./index.html --compile --target=browser --outdir=dist
The generated HTML will inline scripts, styles, and resources.
It can be opened directly with file:// without depending on a web server.
This is suitable for offline demos, one-off tools, and single-page files delivered to clients.
Build analysis report
Version: Provided in 1.3.6, enhanced in 1.4
bun build ./src/index.ts --outdir ./dist --metafile-md=./dist/meta.md
You can also output a JSON format metafile simultaneously.
bun build ./src/index.ts \
--outdir ./dist \
--metafile=./dist/meta.json \
--metafile-md=./dist/meta.md
The Markdown report lists the largest inputs, entry points, dependency chains, and size information.
When troubleshooting bundle size, you can look at this report first before deciding whether to split packages.
--asset: Embed static resources into the executable
Version: New in 1.4
bun build ./build/index.js \
--compile \
--asset ./build/client \
--asset ./build/prerendered \
--outfile server
Resources will be embedded into the compiled executable and accessed through the /$bunfs/ directory tree.
This capability is suitable for packaging frontend static files and backend services into a single distributable file.
It needs to be used with --compile; embedded resources are not an externally writable directory.
ESM bytecode compilation
Version: Provided in 1.3.9, enhanced in 1.4
--bytecode now supports ES modules.
bun build ./app.ts --compile --bytecode --format=esm
--bytecode --format=esm must be used with --compile.
When enabled, top-level await, import.meta, dynamic import, and code splitting can all enter the bytecode-compiled binary.
Previously, --bytecode would force output to CommonJS; now there is no need to compromise.
Code splitting significantly faster
Version: New in 1.4
The reachability traversal for code splitting has been changed to breadth-first, making the complexity O(V+E).
In official benchmarks, for a diamond dependency graph of 20,000 modules, the linking time dropped from 4.65 seconds to 320 milliseconds.
Linear import chains of thousands of modules no longer blow the call stack.
This is an internal improvement to the builder; no configuration changes are needed, it takes effect upon upgrade.
Development Diagnostic Tools
Version: New or enhanced in 1.4
Generate a CPU profile.
bun --cpu-prof app.ts
bun --cpu-prof-md app.ts
Generate a heap snapshot.
bun --heap-prof app.ts
bun --heap-prof-md app.ts
cpuprofile can be opened with Chrome DevTools.
heapsnapshot can be used to troubleshoot memory growth and large objects.
Automatically terminate orphaned child processes when the parent process exits.
bun --no-orphans app.ts
In CI or production startup scripts, you can disable automatic loading of .env.
bun --no-env-file app.ts
Production Runtime and Handy Small Tools
process.on("memoryPressure")
Version: Built-in since 1.3.x, further improved in 1.4
When the operating system is under memory pressure, it notifies Bun, and Bun then emits a memoryPressure event on process.
macOS, Linux, and Windows are all supported.
You can use it to proactively release memory before the system kills the process.
process.on("memoryPressure", () => {
cache.clear();
pool.drainIdle();
});
Common actions are clearing caches, closing idle connections, and stopping temporarily unused workers.
It should be noted that this is a low memory notification from the operating system, not a memory quota guarantee; the release action must be implemented by the business code itself.
bun repl and bun ./README.md
bun repl is a native REPL with syntax highlighting, history, and Tab completion, also supporting -e and -p for direct expression execution.
bun repl
bun repl -p '"1+1: " + (1 + 1)'
There is also an interesting small feature: running a Markdown file directly.
bun ./README.md
Bun will render the Markdown to the terminal without starting the virtual machine at all, so you don't need to install glow to read project documentation.
Other one-liner updates
- The
URLPatternWeb API is built-in, passing 408 web platform tests, replacingpath-to-regexp. Bun.sliceAnsi(),Bun.wrapAnsi(), andBun.stringWidth()handle terminal alignment, accurately calculating Chinese characters, emoji, and ANSI color codes.Bun.spawn({ cgroup })can place a child process into a cgroup on Linux before starting it, for resource limiting.
Rust Rewrite and Performance Changes
Bun 1.4 is the first version after Bun was rewritten in Rust.
The official article states that Claude Code has been running the Rust version for months, and Prisma Compute has also been released based on it.
In terms of performance, the official report includes these results.
- Claude Code's CPU p99 dropped from 24% to 10%.
- Claude Code's CPU p50 dropped from 5.8% to 2.5%.
- Idle CPU usage for a small hello-world service dropped by about 5 times.
- HTTP service memory usage dropped by about 13% to 48%.
- Windows startup time dropped from 39.0 ms to 15.5 ms.
- Linux startup time dropped from 10.9 ms to 5.1 ms.
- Linux peak memory dropped from 33 MB to 14.6 MB.
new URL()is up to 4.6 times faster.Buffer.from(str, "hex")is about 8 times faster.base64urldecoding is up to about 46 times faster.- Promise operations are about 1.5 to 2.4 times faster.
These are benchmark results from the article.
Actual benefits will be affected by code type, data scale, operating system, and hardware.
Node.js Compatibility Continues to Improve
Bun 1.4 has added 1,517 passing tests to the Node.js 26.3.0 test suite.
This indicates that the compatibility scope is expanding, but it does not mean Bun is 100% compatible with Node.js.
The official also specifically mentioned compatibility improvements for Playwright, Next.js 16, Vitest, OpenTelemetry, and dd-trace.
Native addons need to pay attention to the new ABI version.
console.log(process.versions.modules);
// 147
If a project has dependencies that download precompiled native addons based on NODE_MODULE_VERSION, you need to confirm it provides build artifacts for ABI 147.
Security Enhancements
Bun 1.4's security changes are not just icing on the cake, but one of the reasons to upgrade.
The certificate verification callback for fetch() has a more reliable execution timing (new in 1.4).
When you explicitly pass a tls: { checkServerIdentity } callback, it executes after the TLS handshake completes and before the request is written out, and it executes again for each redirect hop.
If the callback returns an Error, fetch() rejects directly, and not a single byte of the request is sent out.
This timing is particularly suitable for certificate pinning, i.e., only trusting certificates with specific fingerprints.
const PINNED = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
await fetch("https://api.example.com/upload", {
method: "POST",
body: secretPayload,
tls: {
checkServerIdentity(_hostname, cert) {
if (cert.fingerprint256 !== PINNED) {
return new Error("pin mismatch");
}
}
}
});
Several other security tightenings are distributed between 1.3.x and 1.4.
Bun.connect() and Bun.listen() now enforce rejectUnauthorized by default when requestCert is enabled (new in 1.4).
tls.connect() uses host as the server name for certificate verification by default (1.3.13).
Redis rediss:// connections verify the hostname by default (1.3.14).
Bun.serve() rejects malformed HTTP framing (1.3.4).
Paths in tar packages that attempt to escape the extraction directory are skipped (1.3.6).
The official also reminds that a few connections that could be established on 1.3 will become certificate verification errors after the upgrade; this is the expected tightening behavior.
When using self-signed certificates or a private CA, explicitly provide the CA.
await fetch("https://internal.example.com", {
tls: {
ca: [Bun.file("./internal-ca.pem")]
}
});
Only disable certificate verification when you clearly know the risks.
await fetch("https://localhost:8443", {
tls: {
rejectUnauthorized: false
}
});
Do not treat rejectUnauthorized: false as a routine fix in production environments.
Platform Support
Bun 1.4 adds native builds for Windows ARM64.
ARM64 Windows devices like Surface, Snapdragon X, and Ampere can run native Bun.
FreeBSD 14.3 and above provide native builds for x86_64 and aarch64.
Android aarch64 and x64 are still experimental support.
The minimum glibc version for Linux has been lowered to 2.17, with a minimum kernel version of 3.10.
Bun has also started preparing for TypeScript 7.
High-precision timers on Windows have also been improved; a 1-millisecond timer now triggers at about 1.4 milliseconds, instead of the previous ~15.5 milliseconds.
Changes Most Likely to Trip You Up During Upgrade
writeHeader() changed to writeHead()
Old code.
res.writeHeader(200, { "Content-Type": "text/plain" });
New code.
res.writeHead(200, { "Content-Type": "text/plain" });
.env is no longer automatically loaded when running as node
This change only affects scenarios where Bun runs with the identity of node, including bun --bun, bunx --bun, and symlinking Bun as node.
These methods now behave like Node.js and no longer automatically read .env files.
If a script depends on .env, pass the file explicitly.
node --env-file=.env ./check.js
When running a script directly with bun ./check.js, .env is still automatically loaded; the behavior has not changed.
New Monorepos default to isolated linker
If you must keep the old hoisted layout, you can explicitly write the configuration.
[install]
linker = "hoisted"
YAML parsing follows YAML 1.2
yes, no, on, and off will be parsed as strings, not booleans.
on: in GitHub Actions will also be treated as a string.
TOML and bunfig.toml are stricter
Unquoted illegal strings, missing newlines between key-value pairs, and integers exceeding Number.MAX_SAFE_INTEGER will all throw a SyntaxError.
bun.lock version upgrade
Bun 1.4's new lockfile is lockfileVersion: 2.
GitHub and tarball dependencies will record SHA-512 integrity information, and Git dependencies will check for path traversal.
Old v0 and v1 lockfiles can still be read; executing bun install once will migrate them.
When using nested overrides or version-qualified overrides, the lockfile will upgrade to lockfileVersion: 3, which older versions of Bun cannot read.
Compiled executables no longer randomly read config from the run directory
Executables generated by bun build --compile no longer automatically read tsconfig.json and package.json from the run directory.
.env and bunfig.toml are still automatically loaded by default and are not affected by this change.
If you really need the old behavior, you can explicitly enable it.
bun build --compile \
--compile-autoload-tsconfig \
--compile-autoload-package-json \
./src/index.ts
Other migration points worth searching for
- In paused mode,
readable.read()without a size returns only one chunk at a time; you need to loop untilnull. fs.rmdir(path, { recursive: true })should be changed tofs.rm(path, { recursive: true, force: true }).clone()onRequestorResponseshould be placed before reading the body.- Network errors from
fetch()are nowTypeError. Bun.cron()uses local time by default; old code relying on UTC needs to explicitly write{ tz: "UTC" }.- Interpolation in
Bun.$no longer automatically expands globs; you need to use explicit glob APIs or command arguments. - MariaDB JSON fields will be directly parsed into objects; do not call
JSON.parse()again.
My Take
The focus of Bun 1.4 is not simply running faster, but reducing the peripheral tools a JavaScript project needs to install and maintain.
Image processing, Markdown, scheduled tasks, PTY, browser automation, static file serving, and configuration parsing are all starting to have built-in solutions.
For new projects, this will make the dependency tree shorter and deployment steps fewer.
For existing projects, what really needs careful evaluation is compatibility and upgrade behavior, not blindly replacing all dependencies.
I wonder what you think? Feel free to leave a comment.
Thank you for reading. I am a rural programmer, independent developer, industry observer, Frontend Tiger Chen Suiyi.