Skip to main content

fast-glob vs tinyglobby vs chokidar: File Globbing and Watching in Node.js (2026)

·PkgPulse Team

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
  • tinyglobby is now preferred for new projects — same API, better ESM support, smaller
  • chokidar uses OS-native watchers (FSEvents on macOS, inotify on Linux) for efficiency

PackageWeekly DownloadsBundle SizeESMPurpose
fast-glob~90M~30KBGlob matching (static)
tinyglobby~30M~15KB✅ NativeGlob matching (static)
chokidar~80M~100KBFile 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

Featurefast-globtinyglobbychokidar
PurposeStatic globStatic globFile watching
Async API✅ Events
Sync API
Stream API
Bundle size~30KB~15KB~100KB
ESM native✅ Native
CJS
Negation patterns
OS-native eventsN/AN/A✅ FSEvents/inotify
Docker/NFS pollingN/AN/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 objectMode for 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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.