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
Performance Characteristics and Large Monorepo Considerations
Glob performance becomes relevant when you are scanning large repositories with tens of thousands of files. fast-glob is highly optimized for this: it uses a micromatch-based pattern matcher, parallelizes directory reads with libuv's thread pool, and has been benchmarked against micromatch, glob, and node-glob to confirm its position as the fastest option in the npm ecosystem. For a monorepo with 50,000 files, fast-glob typically completes in under 100ms while naive glob implementations might take several seconds. tinyglobby's strategy is different — it leverages Node.js's built-in fs.glob() on Node.js 22+ and falls back to a custom implementation on older versions, which means performance is tied to the Node.js version rather than a standalone implementation. In practice, for typical project sizes (under 10,000 files), both fast-glob and tinyglobby complete fast enough that the difference is imperceptible. For very large codebases, fast-glob's streaming API (fg.stream()) provides memory-efficient traversal that begins processing matches before the full scan completes, which is important for pipelines that cannot wait for the full file list.
Platform-Specific Behavior and Cross-Platform Compatibility
File system behavior on Windows, macOS, and Linux differs in ways that affect glob libraries. Case sensitivity is the most common gotcha: macOS and Windows (by default) have case-insensitive file systems, while Linux is case-sensitive. A glob pattern src/**/*.TS finds TypeScript files on macOS but returns nothing on Linux. Both fast-glob and tinyglobby handle this through the caseSensitiveMatch option — setting it to false normalizes behavior across platforms. Path separator handling is another concern: Windows uses backslashes while Unix uses forward slashes. fast-glob normalizes paths to forward slashes in its output regardless of platform, which means globs written in a Linux CI environment work the same on Windows developer machines. chokidar similarly normalizes path separators in its events. One chokidar-specific concern on Windows is that usePolling: false (native FSEvents) uses ReadDirectoryChangesW internally, which has performance implications for large directory trees and can cause issues in Docker Desktop environments on Windows where file system events are forwarded over a network share.
Integration with Build Tooling Ecosystem
Understanding which tools use fast-glob or tinyglobby under the hood helps in troubleshooting and explains the download numbers. fast-glob is a direct dependency of Vite, ESLint, PostCSS, Astro, and Nx, which accounts for its massive weekly download count far exceeding what developers explicitly install. These tools chose fast-glob for its performance and the streaming API that allows them to process large codebases efficiently. chokidar is similarly embedded in webpack (via watchpack), Vite's dev server, Jest (via jest-haste-map on some platforms), and numerous CLI tools that implement watch mode. tinyglobby's adoption is newer but growing — it was introduced as a lighter alternative and has been recommended for new projects by tool authors who want to reduce their dependency footprint. If you are a library author choosing between fast-glob and tinyglobby as a dependency, tinyglobby's smaller size and ESM-native design are compelling; the author of fast-glob has explicitly blessed tinyglobby as the continuation of the glob library lineage for modern projects.
chokidar in Docker and CI Environments
chokidar's behavior in containerized environments deserves special attention. Docker on Linux uses the inotify interface for file system events, which works reliably for files inside the container. However, volumes mounted from the host (common in development setups where your source code directory is mounted into a container) do not propagate inotify events from the host to the container — changes made on the host are not detected by the chokidar watcher running inside the container. The solution is usePolling: true, which checks for file changes by periodically calling stat() on watched files instead of using kernel events. Polling is less efficient (higher CPU usage at interval ms per file) but works reliably across Docker volumes, NFS mounts, and network file systems. The polling interval is configurable — for development servers where latency matters, 300-500ms is typically fast enough to feel responsive. For CI environments that run tests in watch mode and then exit, polling is unnecessary and native events are preferable for speed.
Practical Patterns for Build Tool Authors
If you are building a CLI tool or build plugin that needs to find files, the choice between fast-glob and tinyglobby comes down to your target Node.js version and whether you plan to publish an ESM-only package. For plugins targeting Node.js 20 and below, both libraries work but fast-glob has wider ecosystem recognition — if your users report a glob issue, there is more community knowledge to draw on. For ESM-native packages targeting Node.js 22+, tinyglobby's integration with the built-in fs.glob() and its ESM-native design align better with where the ecosystem is heading. Always add **/node_modules/** to your ignore list explicitly — it is easy to overlook and can cause both slow scans and unexpected matches. For chokidar in watch mode tools, debouncing file change events is essential: editors often write files in multiple operations (write, chmod, rename), triggering multiple chokidar events for a single logical user action. A 50-100ms debounce on the change handler prevents redundant rebuilds.
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 →
See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).