Switching from Node to Bun - Part 1
Build times dropped from 37 seconds to 20 seconds — a 46% improvement — after I switched this blog's codebase from Node.js to Bun. The code got cleaner too. This is Part 1 covering why I switched and the key improvements. Part 2 covers the migration process.
Why Bun?
Native speed: Built on JavaScriptCore (WebKit's engine) instead of V8.
Integrated tooling: Package manager, bundler, test runner in one binary.
TypeScript support: First-class, no extra dependencies.
Modern APIs: Native File and Glob implementations that outperform Node alternatives.
TOML support: Built-in parser for configuration files.
File Operations: Before and After
The biggest wins came from replacing Node's file system operations with Bun's native File API.
Before (Node.js with fs-extra):
// Check if file paths exist
if (await fs.pathExists(withSlash)) {
filePath = withSlash;
} else if (await fs.pathExists(withoutSlash)) {
filePath = withoutSlash;
}
// Read file content
const content = await fs.readFile(filePath);
let htmlContent = content.toString();
After (Bun's File API):
// Check if file paths exist
if (await Bun.file(withSlash).exists()) {
filePath = withSlash;
} else if (await Bun.file(withoutSlash).exists()) {
filePath = withoutSlash;
}
// Read file content
const bunFile = Bun.file(filePath);
let htmlContent = await bunFile.text();
The Bun version runs faster. The native File API handles existence checks and reads with less overhead than Node's implementation.
Bun's Native Glob API
I replaced recursive directory traversal with Bun's native Glob API.
Before (Node.js recursion):
async function getMarkdownFilesRecursively(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(
entries.map(async (entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
return getMarkdownFilesRecursively(fullPath);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
return [fullPath];
}
return [];
}),
);
return files.flat();
}
After (Bun's Glob API):
async function getMarkdownFilesRecursively(dir: string): Promise<string[]> {
const glob = new Bun.Glob("**/*.md");
const files: string[] = [];
for await (const file of glob.scan({
cwd: dir,
absolute: true,
})) {
files.push(file);
}
return files;
}
Tip
The Bun implementation is simpler and faster — fewer lines, fewer edge cases, easier to maintain.
Why Bun Is Fast
JavaScriptCore engine: Apple's JSC engine outperforms V8 for many operations.
Native implementations: File, HTTP, and core APIs are written in Zig, not JavaScript. No abstraction layers.
Optimized I/O: Bun's file operations bypass Node's libuv layer.
Single binary: Runtime, bundler, and package manager share one process. No inter-process communication overhead.
Built-in TypeScript: No separate compilation step.
Zero-copy architecture: Minimizes memory copies, especially for file operations.
These architectural choices compound. I/O-intensive tasks like static site generation benefit the most.
Continue to Part 2 for the migration process, challenges, and results.