Last Updated: 2025-11-01 Compared Runtimes: Bun 1.x, Node.js 25.x, Deno 2.x
This document provides a comprehensive comparison of how Bun, Node.js, and Deno handle module resolution, configuration, and dependency management. Use this guide to understand the trade-offs when choosing a runtime or when building tools that need to work across multiple runtimes.
| Requirement | Best Choice | Why |
|---|---|---|
| Maximum compatibility | Node.js | Industry standard, widest ecosystem |
| Performance & DX | Bun | Fastest, TypeScript native, Node.js compatible |
| Security & Modern | Deno | Permissions-based security, URL imports, no node_modules |
| TypeScript projects | Bun or Deno | Native TS support, no compilation step |
| Legacy projects | Node.js or Bun | Bun offers Node.js compatibility with better performance |
| New greenfield projects | Deno | Modern design, secure by default, clean dependency management |
Algorithm: Classic node_modules directory traversal
Key characteristics:
- Walks up directory tree looking for
node_modules/ - Falls back to global folders (
$HOME/.node_modules,$PREFIX/lib/node) - Supports both CommonJS and ES modules
- Different resolution for
require()vsimport
Search path example for /home/user/project/src/app.js requiring lodash:
/home/user/project/src/node_modules/lodash
/home/user/project/node_modules/lodash
/home/user/node_modules/lodash
/home/node_modules/lodash
/node_modules/lodash
$HOME/.node_modules/lodash
$PREFIX/lib/node/lodash
File resolution for require('./module'):
./module.js./module.json./module.node./module/package.json→ readmainfield./module/index.js./module/index.json./module/index.node
Algorithm: Implements Node.js algorithm + TypeScript path mapping
Key characteristics:
- 100% Node.js compatible - same search algorithm
- Native TypeScript support - searches for
.ts/.tsxfirst - Respects
tsconfig.jsonpaths (unlike Node.js) - Uses global cache with hardlinks for efficiency
- Supports
buncondition in package.json exports
File resolution priority:
.tsx.jsx.ts.mjs.js.cjs.jsonindex.<ext>in same order
Cache strategy:
- Global cache:
~/.bun/install/cache/ - Uses hardlinks to project
node_modules/ - All projects share single instance of each package version
- Reduces disk usage and improves install speed
Algorithm: URL-based, no node_modules
Key characteristics:
- No directory traversal - uses import maps
- Mandatory file extensions
- Direct HTTPS imports supported
- Multiple registries (JSR, npm, URLs)
- Global cache only (no project-local dependencies)
Resolution sources:
- Import maps in
deno.json- maps bare specifiers to URLs/paths - Relative/absolute paths - must include file extension
- HTTPS URLs - downloaded and cached automatically
- JSR packages -
jsr:@scope/package - npm packages -
npm:package-name
Cache location:
- macOS:
~/Library/Caches/deno - Linux:
~/.cache/deno - Windows:
%LOCALAPPDATA%\deno
| Variable | Node.js | Bun | Deno | Purpose |
|---|---|---|---|---|
NODE_PATH |
✅ Legacy | ✅ Supported | ❌ | Additional module search paths |
NODE_OPTIONS |
✅ | ✅ | ❌ | Runtime options |
BUN_INSTALL |
❌ | ✅ | ❌ | Bun installation directory |
BUN_INSTALL_CACHE_DIR |
❌ | ✅ | ❌ | Package cache location |
BUN_INSTALL_GLOBAL_DIR |
❌ | ✅ | ❌ | Global packages directory |
DENO_DIR |
❌ | ❌ | ✅ | Deno cache directory |
DENO_AUTH_TOKENS |
❌ | ❌ | ✅ | Registry authentication |
NO_COLOR |
✅ | ✅ | ✅ | Disable colored output |
| Feature | Node.js | Bun | Deno |
|---|---|---|---|
| Primary config | package.json |
package.json + bunfig.toml |
deno.json |
| TypeScript paths | ❌ Needs ts-node | ✅ Native | ✅ Via import maps |
| Import maps | ❌ | ❌ | ✅ Native |
| Lock files | package-lock.json |
bun.lockb (binary) |
deno.lock |
| Global config | ~/.npmrc |
~/.bunfig.toml |
N/A |
Node.js package.json:
{
"type": "module",
"main": "./dist/index.js",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}Bun package.json (extends Node.js):
{
"type": "module",
"main": "./dist/index.js",
"exports": {
".": {
"bun": "./src/index.ts", // Bun-specific (TypeScript)
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"dependencies": {
"express": "^4.18.0"
}
}Deno deno.json:
{
"imports": {
"express": "npm:express@^4.18.0",
"std/": "https://deno.land/std@0.208.0/",
"@/": "./src/"
},
"tasks": {
"dev": "deno run --watch main.ts"
},
"compilerOptions": {
"strict": true
}
}Key differences:
- Deno uses
imports(import maps) instead ofdependencies - Deno doesn't separate dev/prod dependencies (unused code not loaded)
- Bun adds
buncondition for TypeScript entry points
Local (preferred):
/home/user/project/node_modules/
Global (legacy, discouraged):
/usr/local/lib/node_modules/
$HOME/.node_modules/
$HOME/.node_libraries/
$PREFIX/lib/node/
Install:
npm install express # Local
npm install -g typescript # GlobalLocal:
/home/user/project/node_modules/ # Hardlinked from cache
Global:
~/.bun/install/global/node_modules/
Cache (all projects share):
~/.bun/install/cache/
Install:
bun install express # Local (hardlinked)
bun add --global typescript # GlobalNo local modules - everything is cached globally:
macOS: ~/Library/Caches/deno/
Linux: ~/.cache/deno/
Windows: %LOCALAPPDATA%\deno
Cache structure:
$DENO_DIR/
├── deps/ # Remote URLs
├── npm/ # npm packages
└── gen/ # Compiled code
"Install":
# Just cache dependencies
deno cache main.ts
# Add to deno.json
deno add @std/path
deno add npm:expressNode.js:
// Resolve module path
const path = require.resolve('lodash');
// /home/user/project/node_modules/lodash/lodash.js
// Get search paths
const paths = require.resolve.paths('lodash');
// ['/home/user/project/node_modules', '/home/user/node_modules', ...]
// Custom search paths
const custom = require.resolve('my-module', {
paths: ['/custom/path']
});
// Module paths
console.log(module.paths);
// Array of node_modules directories that would be searchedBun:
// Synchronous resolution
const path = Bun.resolveSync("lodash", "/path/to/project");
// /path/to/project/node_modules/lodash/index.ts
// Async resolution
const path = await Bun.resolve("./module.ts", import.meta.dir);
// Resolve from CWD
const cwdPath = Bun.resolveSync("express", process.cwd());
// Resolve from current file
const localPath = Bun.resolveSync("./utils", import.meta.dir);Deno:
// Resolve with import maps
const resolved = import.meta.resolve('std/path');
// https://deno.land/std@0.208.0/path/mod.ts
// Resolve relative
const path = import.meta.resolve('./utils.ts');
// file:///home/user/project/utils.ts
// Dynamic import
const module = await import(import.meta.resolve('./dynamic.ts'));Node.js:
- No built-in plugin system for resolution
- Tools like webpack/rollup provide custom resolvers
Bun:
import type { BunPlugin } from "bun";
const plugin: BunPlugin = {
name: "custom-resolver",
setup(build) {
build.onResolve({ filter: /^custom:/ }, (args) => ({
path: args.path.replace("custom:", "/real/path/"),
namespace: "custom"
}));
}
};
Bun.plugin(plugin);Deno:
- No plugin system for resolution
- Use import maps for path customization
| Feature | Node.js | Bun | Deno |
|---|---|---|---|
| Implicit extensions | ✅ CommonJS only | ✅ Always | ❌ Never |
| Extension search order | .js, .json, .node |
.tsx, .ts, .js, .json |
N/A (explicit) |
| TypeScript support | ❌ Needs tools | ✅ Native | ✅ Native |
| index.js auto-load | ✅ | ✅ | ❌ |
Examples:
// Node.js CommonJS
require('./module') // ✅ Finds module.js
require('./module.js') // ✅ Explicit
// Node.js ESM
import { x } from './module' // ❌ Error
import { x } from './module.js' // ✅ Required
// Bun (both work)
import { x } from './module' // ✅ Finds module.ts/js
import { x } from './module.ts' // ✅ Explicit
// Deno (explicit required)
import { x } from './module' // ❌ Error
import { x } from './module.ts' // ✅ RequiredNode.js:
.mjs→ Always ES module.cjs→ Always CommonJS.js→ Depends on nearestpackage.jsontypefield"type": "module"→ ESM"type": "commonjs"or missing → CommonJS
Bun:
- Same as Node.js for compatibility
- Additionally recognizes
.ts,.tsx,.jsxnatively
Deno:
.ts,.tsx,.js,.jsx→ Always ES modules- No CommonJS support
Node.js:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}// ❌ Doesn't work in Node.js runtime
import { utils } from '@/utils';
// Requires ts-node, tsx, or compilationBun:
// tsconfig.json (same as Node.js)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}// ✅ Works natively in Bun
import { utils } from '@/utils';Deno:
// deno.json (import maps)
{
"imports": {
"@/": "./src/"
}
}// ✅ Works with import maps
import { utils } from '@/utils.ts';Winner: Bun for TypeScript path compatibility, Deno for explicit configuration
Benchmarks (installing React app dependencies):
- Bun: ~0.5s (fastest)
- pnpm: ~2.0s
- Yarn: ~3.5s
- npm: ~12s (slowest)
| Runtime | Lockfile | Format | Readable |
|---|---|---|---|
| Node.js (npm) | package-lock.json |
JSON | ✅ |
| Node.js (yarn) | yarn.lock |
Custom | ✅ |
| Node.js (pnpm) | pnpm-lock.yaml |
YAML | ✅ |
| Bun | bun.lockb |
Binary | ❌ |
| Deno | deno.lock |
JSON | ✅ |
node_modules size (typical React app):
npm: ~200MB in node_modules + duplicates across projects
pnpm: ~100MB (hardlinks, shared store)
Bun: ~50MB (hardlinks, global cache)
Deno: ~0MB (no node_modules, global cache only)
| Source | Node.js | Bun | Deno |
|---|---|---|---|
| npm registry | ✅ Default | ✅ Default | ✅ npm: prefix |
| Git repos | ✅ | ✅ | ✅ HTTPS |
| Tarball URLs | ✅ | ✅ | ✅ |
| HTTPS URLs | ❌ | ❌ | ✅ Native |
| JSR | ❌ | ❌ | ✅ Native |
| Local paths | ✅ file: |
✅ file: |
✅ Relative |
Package cache:
~/.npm/_cacache/
Module runtime cache:
// In-memory cache, accessible via:
require.cacheBehavior:
- Packages downloaded to cache, then copied to
node_modules/ - Each project has full copy (no sharing)
- Runtime cache is per-process
Package cache:
~/.bun/install/cache/
Linking strategy:
~/.bun/install/cache/lodash@4.17.21/ (master copy)
↓ hardlink
/project/node_modules/lodash/ (instant, no disk duplication)
Transpilation cache:
$BUN_RUNTIME_TRANSPILER_CACHE_PATH/
Benefits:
- Zero-copy installs (hardlinks)
- All projects share single instance
- Massive disk space savings
- Faster installs
Linker options:
bun install # Default: hardlinks
bun install --linker=isolated # pnpm-style isolationGlobal cache only:
macOS: ~/Library/Caches/deno/
Linux: ~/.cache/deno/
Windows: %LOCALAPPDATA%\deno
Structure:
$DENO_DIR/
├── deps/
│ └── https/ # URL imports
├── npm/ # npm packages
├── gen/ # Compiled JS
└── registries/jsr/ # JSR packages
Behavior:
- All dependencies cached globally
- No project-local copies
- Shared across all projects
- Can be locked with
deno.lock
Cache management:
deno cache main.ts # Download and cache
deno cache --reload main.ts # Force refresh
deno info # Show cache locationSecurity: None built-in
node app.js # Full system access by defaultImplications:
- Packages can access filesystem, network, environment variables freely
- Supply chain attacks are high risk
- Must trust all dependencies
Mitigations:
- Use
npm audit - Lock dependencies with
package-lock.json - Use tools like Snyk, Dependabot
Security: Same as Node.js (for compatibility)
bun run app.ts # Full system accessNote: Bun prioritizes Node.js compatibility over security sandboxing.
Security: Permission-based (default deny)
# ❌ No permissions - will fail if app needs them
deno run app.ts
# ✅ Explicit permissions
deno run --allow-read --allow-write --allow-net app.ts
# Allow specific domains
deno run --allow-net=api.example.com app.ts
# Allow specific directories
deno run --allow-read=/tmp app.tsPermissions:
--allow-read[=<path>]- Filesystem read--allow-write[=<path>]- Filesystem write--allow-net[=<domain>]- Network access--allow-env[=<var>]- Environment variables--allow-run[=<cmd>]- Subprocess execution--allow-all- All permissions (not recommended)
Benefits:
- Untrusted code sandboxed by default
- Clear visibility into what dependencies can access
- Reduced supply chain attack surface
Benchmarks (resolving 1000 modules):
- Bun: ~5ms (native code, optimized)
- Node.js: ~15ms (baseline)
- Deno: ~10ms (Rust-based)
Cold start (simple app):
- Bun: ~20ms
- Deno: ~30ms
- Node.js: ~50ms
With large dependency tree:
- Bun: ~100ms
- Deno: ~150ms
- Node.js: ~300ms
Idle runtime:
- Bun: ~30MB
- Deno: ~40MB
- Node.js: ~50MB
With dependencies:
- Bun: ~60MB
- Deno: ~70MB
- Node.js: ~100MB
| Ecosystem | Node.js | Bun | Deno |
|---|---|---|---|
| npm packages | ✅ 100% | ✅ ~95% | |
| Native addons | ✅ Full | ❌ Not supported | |
| TypeScript packages | ✅ Native | ✅ Native | |
| Deno packages | ❌ | ❌ | ✅ |
| JSR packages | ❌ | ❌ | ✅ |
| API | Node.js | Bun | Deno |
|---|---|---|---|
fs |
✅ | ✅ | ✅ node:fs |
path |
✅ | ✅ | ✅ node:path |
http |
✅ | ✅ | ✅ node:http |
crypto |
✅ | ✅ | ✅ node:crypto |
child_process |
✅ | ✅ |
Bun: Near 100% Node.js API compatibility
Deno: Good compatibility via node: imports
✅ Use Node.js when:
- Maximum ecosystem compatibility required
- Using native Node.js addons
- Working with legacy codebases
- Team familiarity with npm/yarn
- Enterprise support needed
- Long-term stability critical
❌ Avoid Node.js when:
- Performance is critical
- TypeScript-first development
- Want faster iteration/installs
- Building new greenfield projects
✅ Use Bun when:
- Performance is priority
- TypeScript/JSX projects
- Want Node.js compatibility + speed
- Fast development iteration needed
- Disk space is concern (global cache)
- Using tsconfig.json paths
❌ Avoid Bun when:
- Need maximum stability (Bun still maturing)
- Using many native addons
- Enterprise/conservative environment
- Need LTS support
✅ Use Deno when:
- Security is critical
- Building new projects from scratch
- Want modern development experience
- TypeScript-first development
- Prefer URL imports and import maps
- No legacy Node.js dependencies
❌ Avoid Deno when:
- Heavy npm dependency requirements
- Need Node.js API compatibility
- Using native addons
- Team unfamiliar with Deno paradigms
- Working with existing Node.js codebase
Effort: Low (90% compatible)
Steps:
- Install Bun:
curl -fsSL https://bun.sh/install | bash - Replace
nodewithbun:bun run app.js - Replace
npm installwithbun install - Test thoroughly (check native addons)
Benefits:
- Faster execution
- Faster installs
- Native TypeScript
- Minimal code changes
Risks:
- Native addon incompatibility
- Edge case differences
- Less mature ecosystem
Effort: High (requires code changes)
Steps:
- Install Deno:
curl -fsSL https://deno.land/install.sh | sh - Create
deno.jsonwith import maps - Update imports to use file extensions
- Replace npm packages with JSR or
npm:prefix - Add permissions to run commands
- Rewrite package.json scripts as deno tasks
Benefits:
- Modern, secure runtime
- No node_modules
- Better TypeScript experience
- Built-in tooling (fmt, lint, test)
Risks:
- Significant refactoring required
- npm package compatibility issues
- Team learning curve
Effort: Very Low
Steps:
- Replace
bunwithnode - Use
npminstead ofbun install - Remove Bun-specific features (if any)
Why migrate back:
- Bun compatibility issues
- Need enterprise LTS support
- Native addon requirements
| Feature | Node.js | Bun | Deno |
|---|---|---|---|
| Module algorithm | node_modules tree | node_modules tree | Import maps + cache |
| TypeScript | Needs tools | ✅ Native | ✅ Native |
| Path mapping | Needs tools | ✅ Native | ✅ Import maps |
| File extensions | Optional (CJS) | Optional | ✅ Required |
| npm packages | ✅ Native | ✅ Native | npm: prefix |
| URL imports | ❌ | ❌ | ✅ Native |
| Install speed | Slow | ✅ Fastest | Medium |
| Disk usage | High | Low (cache) | ✅ Lowest (no node_modules) |
| Security | None | None | ✅ Permissions |
| Compatibility | ✅ 100% | ~95% Node.js | ~70% npm |
| Maturity | ✅ Very mature | Maturing | Mature |
| Performance | Baseline | ✅ Fastest | Fast |
Choose based on your priorities:
- Ecosystem & Stability → Node.js
- Performance & DX → Bun
- Security & Modern → Deno
All three runtimes are production-ready, but serve different use cases. Node.js remains the safe, compatible choice. Bun offers a faster, more pleasant development experience with minimal migration cost. Deno represents a clean-slate redesign prioritizing security and modern standards.
For most projects, Bun offers the best balance of performance, developer experience, and ecosystem compatibility. For security-critical or greenfield projects, Deno is worth serious consideration. Node.js remains the most battle-tested and widely supported option.