fast-glob vs tinyglobby vs chokidar: File Globbing and Watching in Node.js (2026)
TL;DR
fast-glob is the most widely used glob library — fast, feature-complete, supports streams, and used by Vite, ESLint, PostCSS, and hundreds of other tools. tinyglobby is the modern, lighter alternative — ESM-native, same API surface, and recommended as the drop-in replacement by the fast-glob author himself. chokidar solves a different problem — it watches the file system for changes (using native OS events) rather than just matching existing files. For finding files: tinyglobby for new projects, fast-glob for existing code. For watching files: chokidar.
Key Takeaways
- fast-glob: ~90M weekly downloads — the glob standard, used by Vite/ESLint/PostCSS
- tinyglobby: ~30M weekly downloads — modern replacement, ESM-native, actively growing
- chokidar: ~80M weekly downloads — file system watcher (different use case from globbing)
- Node.js 22+ has
glob()built-in — fine for simple cases, limited vs fast-glob/tinyglobby tinyglobbyis now preferred for new projects — same API, better ESM support, smaller- chokidar uses OS-native watchers (FSEvents on macOS, inotify on Linux) for efficiency
Download Trends
| Package | Weekly Downloads | Bundle Size | ESM | Purpose |
|---|---|---|---|---|
fast-glob | ~90M | ~30KB | ✅ | Glob matching (static) |
tinyglobby | ~30M | ~15KB | ✅ Native | Glob matching (static) |
chokidar | ~80M | ~100KB | ✅ | File system watching |
fast-glob
fast-glob — the most popular glob library in the Node.js ecosystem:
Basic usage
import fg from "fast-glob"
// Find all TypeScript files:
const tsFiles = await fg("src/**/*.ts")
// ["src/index.ts", "src/utils/helpers.ts", "src/components/Button.ts"]
// Multiple patterns:
const files = await fg(["src/**/*.ts", "src/**/*.tsx"])
// Negation patterns (exclude):
const nonTest = await fg(["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.spec.ts"])
// Include dotfiles (e.g., .env, .gitignore):
const dotfiles = await fg(["**/*.{env,gitignore}"], { dot: true })
// Case insensitive (Windows-friendly):
const jsFiles = await fg("src/**/*.JS", { caseSensitiveMatch: false })
Options
import fg from "fast-glob"
const files = await fg("src/**/*", {
cwd: "/path/to/project", // Base directory (default: process.cwd())
dot: false, // Include dotfiles? (default: false)
deep: 3, // Max directory depth (default: Infinity)
followSymbolicLinks: true, // Follow symlinks (default: true)
onlyFiles: true, // Files only (default: true)
onlyDirectories: false, // Directories only (default: false)
ignore: ["**/node_modules/**"], // Always exclude these patterns
absolute: false, // Return absolute paths? (default: false)
objectMode: false, // Return Entry objects with stats? (default: false)
})
Entry objects (with metadata)
import fg from "fast-glob"
// Get file metadata alongside paths:
const entries = await fg("content/**/*.mdx", {
objectMode: true, // Returns DirentEntry objects
})
for (const entry of entries) {
console.log(entry.path) // "content/blog/react-vs-vue.mdx"
console.log(entry.name) // "react-vs-vue.mdx"
console.log(entry.dirent.isFile()) // true
}
Synchronous API
import fg from "fast-glob"
// Sync version (blocks — use in build scripts where blocking is OK):
const files = fg.sync("src/**/*.ts")
const entries = fg.sync("content/**/*.mdx", { objectMode: true })
// Stream version (memory-efficient for large directories):
const stream = fg.stream("**/*.ts")
stream.on("data", (path) => console.log(path))
stream.on("end", () => console.log("Done"))
Common patterns in build tools
import fg from "fast-glob"
import path from "path"
// Find all MDX content files for Next.js:
async function getAllBlogSlugs() {
const files = await fg("content/blog/**/*.mdx")
return files.map((file) => {
// "content/blog/react-vs-vue.mdx" → "react-vs-vue"
return path.basename(file, ".mdx")
})
}
// Find all route files for a framework:
async function getRouteFiles() {
return fg("app/**/page.{ts,tsx}", {
ignore: ["app/api/**"],
})
}
// Check if files exist matching a pattern:
async function hasTestFiles(dir: string) {
const tests = await fg(`${dir}/**/*.{test,spec}.{ts,tsx}`)
return tests.length > 0
}
tinyglobby
tinyglobby — the modern, lighter glob alternative:
Basic usage
import { glob, globSync } from "tinyglobby"
// Same patterns as fast-glob:
const files = await glob("src/**/*.ts")
// With options:
const mdxFiles = await glob(["content/**/*.mdx", "!content/drafts/**"], {
cwd: process.cwd(),
dot: false,
absolute: false,
})
// Sync:
const filesSync = globSync("src/**/*.ts")
Key differences from fast-glob
import { glob } from "tinyglobby"
// tinyglobby uses native Node.js fs.glob() under the hood on Node.js 22+
// — falls back to custom implementation on older versions
// Same patterns, same options:
const files = await glob("**/*.{ts,tsx}", {
ignore: ["**/node_modules/**", "**/*.d.ts"],
dot: false,
})
// Pattern: tinyglobby is the recommended replacement for fast-glob in new projects
// The fast-glob author acknowledged tinyglobby as the better maintained option for 2026
Migration from fast-glob
// Before (fast-glob):
import fg from "fast-glob"
const files = await fg("src/**/*.ts", { dot: true })
const sync = fg.sync("src/**/*.ts")
// After (tinyglobby):
import { glob, globSync } from "tinyglobby"
const files = await glob("src/**/*.ts", { dot: true })
const sync = globSync("src/**/*.ts")
// API is nearly identical — most migrations are 2-line changes
chokidar
chokidar — the file system watcher (different use case):
Basic watching
import chokidar from "chokidar"
// Watch a directory for changes:
const watcher = chokidar.watch("src", {
ignored: /(^|[/\\])\../, // ignore dotfiles
persistent: true, // keep process alive
ignoreInitial: false, // emit events for existing files on startup
})
// File events:
watcher
.on("add", (path) => console.log(`File added: ${path}`))
.on("change", (path) => console.log(`File changed: ${path}`))
.on("unlink", (path) => console.log(`File removed: ${path}`))
// Directory events:
watcher
.on("addDir", (path) => console.log(`Directory added: ${path}`))
.on("unlinkDir", (path) => console.log(`Directory removed: ${path}`))
// Error and ready:
watcher
.on("error", (error) => console.error(`Watcher error: ${error}`))
.on("ready", () => console.log("Initial scan complete. Watching for changes..."))
Dev server with auto-rebuild
import chokidar from "chokidar"
import { execSync } from "child_process"
// Watch and rebuild on change:
const watcher = chokidar.watch(["src/**/*.ts", "content/**/*.mdx"], {
ignored: /node_modules/,
ignoreInitial: true, // Don't trigger on startup
})
let building = false
watcher.on("change", async (filePath) => {
if (building) return // Debounce concurrent builds
building = true
console.log(`[${new Date().toLocaleTimeString()}] ${filePath} changed — rebuilding...`)
try {
execSync("npm run build", { stdio: "inherit" })
console.log("Build complete")
} catch (err) {
console.error("Build failed")
} finally {
building = false
}
})
console.log("Watching for changes...")
Watch options
import chokidar from "chokidar"
const watcher = chokidar.watch(".", {
// Performance:
usePolling: false, // Use native OS events (faster, less CPU) — set true for Docker/NFS
interval: 100, // Polling interval (ms) when usePolling: true
binaryInterval: 300, // Polling interval for binary files
// Behavior:
persistent: true, // Keep process alive while watching
ignoreInitial: true, // Don't emit 'add' for pre-existing files
followSymlinks: true, // Watch symlink targets
depth: undefined, // Max recursion depth (undefined = unlimited)
// Filtering:
ignored: [
"**/node_modules/**",
"**/.git/**",
"**/.DS_Store",
],
awaitWriteFinish: {
// Wait for file write to complete before triggering event:
stabilityThreshold: 200, // ms after last change before emit
pollInterval: 100, // ms between checks
},
})
Stop watching
// Programmatic stop:
setTimeout(() => {
watcher.close().then(() => console.log("Watcher stopped"))
}, 30000) // Stop after 30 seconds
// Add/remove watched paths dynamically:
watcher.add("new-directory/**")
watcher.unwatch("old-directory/**")
// List watched paths:
const watched = watcher.getWatched()
// { "src": ["index.ts", "utils/helpers.ts"], ... }
Feature Comparison
| Feature | fast-glob | tinyglobby | chokidar |
|---|---|---|---|
| Purpose | Static glob | Static glob | File watching |
| Async API | ✅ | ✅ | ✅ Events |
| Sync API | ✅ | ✅ | ❌ |
| Stream API | ✅ | ❌ | ❌ |
| Bundle size | ~30KB | ~15KB | ~100KB |
| ESM native | ✅ | ✅ Native | ✅ |
| CJS | ✅ | ✅ | ✅ |
| Negation patterns | ✅ | ✅ | ✅ |
| OS-native events | N/A | N/A | ✅ FSEvents/inotify |
| Docker/NFS polling | N/A | N/A | ✅ usePolling |
| TypeScript | ✅ | ✅ | ✅ |
Node.js Built-in glob()
Node.js 22+ ships with a built-in glob() function — no install needed for simple cases:
import { glob } from "node:fs/promises"
// Simple patterns:
const files = await Array.fromAsync(glob("**/*.ts"))
// With options:
const tsFiles = await Array.fromAsync(glob("src/**/*.ts", {
exclude: ["**/*.test.ts"],
cwd: process.cwd(),
withFileTypes: false,
}))
// Limitations vs fast-glob/tinyglobby:
// - No streaming API
// - Less brace expansion support
// - Slower performance on large directories
// - Fewer options
// For build tools and scripts with performance requirements, still prefer fast-glob or tinyglobby
When to Use Each
Choose tinyglobby if:
- Starting a new project — it's the recommended modern choice
- ESM-native is important to your toolchain
- You want a lighter fast-glob replacement (15KB vs 30KB)
- Simple to moderate glob patterns
Choose fast-glob if:
- Already using it in an existing project — no reason to migrate
- You need the streaming API for memory-efficient large directory traversal
- You need
objectModefor file metadata alongside paths - Maximum ecosystem compatibility (everything knows fast-glob's API)
Choose chokidar if:
- You need to watch files for changes, not just match them
- Building a dev server, build watcher, or hot-reload system
- You need OS-native events (not polling) for performance
- Docker/NFS environments where you need polling fallback
Use Node.js glob() if:
- Node.js 22+ and simple patterns without performance requirements
- Avoiding dependencies is the priority
- Basic scripts that don't need streaming or advanced options
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on fast-glob v3.x, tinyglobby v0.2.x, and chokidar v4.x.
Compare file system and developer tool packages on PkgPulse →