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: File and Glob APIs that simplified this generator.
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.
// Check if file paths exist
-if (await fs.pathExists(withSlash)) {
+if (await Bun.file(withSlash).exists()) {
filePath = withSlash;
-} else if (await fs.pathExists(withoutSlash)) {
+} else if (await Bun.file(withoutSlash).exists()) {
filePath = withoutSlash;
}
// Read file content
-const content = await fs.readFile(filePath);
-let htmlContent = content.toString();
+const bunFile = Bun.file(filePath);
+let htmlContent = await bunFile.text();
The Bun version is more direct. The end-to-end build improved, but I did not benchmark these reads separately, so I cannot assign the speedup to this API alone.
Bun’s Native Glob API
I replaced recursive directory traversal with Bun’s native Glob API.
async function getMarkdownFilesRecursively(dir: string): Promise<string[]> {
- const entries = await fs.readdir(dir, { withFileTypes: true });
+ const glob = new Bun.Glob("**/*.md");
+ const files: string[] = [];
- 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 [];
- }),
- );
+ for await (const file of glob.scan({
+ cwd: dir,
+ absolute: true,
+ })) {
+ files.push(file);
+ }
- return files.flat();
+ 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.
What the timing proves
The 37-to-20-second result belongs to this generator and workload. It does not isolate the runtime engine from the file-API changes, dependency removal, or cache effects. Bun’s built-in TypeScript execution also does not replace a type checker.
Was this useful?
Thanks for the feedback.