esbuild can paginate through millions of files in a monorepo faster than you can say "transpilation" by leveraging a unique, parallelized file watching and compilation strategy that avoids deep directory traversal.
Let’s see it in action. Imagine a monorepo with three packages: app, ui, and utils.
/monorepo
/packages
/app
index.ts
package.json
/ui
button.tsx
package.json
/utils
math.ts
package.json
package.json
Here’s how you’d configure esbuild to watch and build these:
// esbuild.config.json
{
"bundle": true,
"entryPoints": [
"packages/app/index.ts",
"packages/ui/button.tsx"
],
"outdir": "dist",
"platform": "node",
"format": "cjs",
"watch": {
"onRebuild": (error, result) => {
if (error) {
console.error("Build failed:", error);
} else {
console.log("Build succeeded:", result);
}
}
}
}
And to run it:
esbuild --config=esbuild.config.json --watch
When you change packages/app/index.ts, esbuild doesn’t re-scan the entire /monorepo directory. Instead, it maintains an internal map of watched files and their dependencies. It detects the change in index.ts, identifies its direct dependencies (e.g., ui/button.tsx if imported), and only recompiles those specific files and their dependents. This granular approach is key to its speed.
The problem esbuild solves here is the combinatorial explosion of build times in large codebases. Traditional bundlers often re-scan large portions of the file system on every change, leading to minutes of wait time for even minor edits in a monorepo. esbuild’s design tackles this by:
- Parallel File Watching: It doesn’t rely on a single OS-level file watcher that might struggle with hundreds of thousands of files. Instead, it uses a highly optimized, multi-threaded watcher that can efficiently monitor individual files and directories relevant to the current build.
- Dependency Graph: As soon as a file is compiled, esbuild builds a precise dependency graph. This graph is not a loose approximation but a concrete map of which modules import which other modules.
- Incremental Rebuilds: When a file changes, esbuild traverses this graph backwards from the changed file to identify all modules that directly or indirectly depend on it. Only these affected modules are recompiled. This avoids unnecessary work on unaffected parts of the codebase.
- Pre-bundling: For very large projects, esbuild can pre-bundle dependencies. This means external libraries (like
reactorlodash) are processed once and then treated as static assets, reducing the amount of code esbuild needs to analyze and transform during incremental builds.
The core levers you control are the entryPoints, platform, format, and crucially, the watch configuration. The entryPoints define the starting points of your application or library, and esbuild works outwards from there. platform (node, browser, neutral) and format (esm, cjs, iife) dictate the output environment and module system. The watch configuration, with its onRebuild hook, is where you see the incremental nature in action, providing feedback on successful or failed rebuilds.
What most people miss is that esbuild’s speed isn’t just about being "fast." It’s about its explicit design to minimize the scope of work on every change. It actively avoids re-evaluating parts of the dependency graph that haven’t been impacted. This means that even if you have thousands of packages in your monorepo, if you only change a single file in one small package, only that package and its direct consumers will be rebuilt. This is a fundamental shift from bundlers that might re-scan directories or re-evaluate broader scopes.
The next challenge you’ll encounter is optimizing the build output for production, which involves advanced tree-shaking and code splitting strategies.