Writing Technical guide

Performance · 2 min read

Bun Cut This Blog's Build Time Nearly in Half

Moving this blog from Node.js to Bun cut its build from 37 seconds to 20. Native file and glob APIs also removed code I no longer needed.

In this article4 sections

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?

What was missing?

Thanks for the feedback.