This blog took 37 seconds to build on Node.js. After I moved it to Bun, the same build took 20 seconds. The native file and glob APIs also let me delete traversal code and a dependency. This first part covers the changes that mattered. Part 2 covers the migration.
Why Bun Fit This Codebase
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;
}
The Bun version replaces recursion and manual flattening with one glob. That is the kind of migration win I care about: the measured build got faster and there is less code left to own.
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 fit this I/O-heavy static-site generator well. I would not assume the same improvement for every Node application; the 37-to-20-second result belongs to this codebase and workload.
Continue to Part 2 for the migration process, challenges, and results.
One quick signal
Did this earn your time?
Thanks. That gives me something concrete to check.


