Technical guide
25

Technical guide · Published Apr 29, 2025

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.

A field-guide drawing of a canvas ridge tent
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: 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?

What was missing?

Thanks. That gives me something concrete to check.