Plate 08
fast-glob vs globby: 0.7 ms vs 14 ms With gitignore
Aditya Challa6 min read
I timed both glob libraries on this box against the same 7,782-file repo copy: with node_modules excluded by an ignore pattern, fast-glob 3.3.3 returned 481 .js files in about 0.72 ms and globby 16.2.4 in about 1.22 ms. Turning on globby's gitignore: true gave the same 481 files but took 14.3–15.9 ms, roughly 20× the fast-glob call.
Short answer: use fast-glob (or tinyglobby) when you can write the ignore list yourself, and pay for globby when you need .gitignore rules honored or directory arguments expanded. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 03:00 IST.
- Versions: fast-glob 3.3.3, globby 16.2.4, tinyglobby 0.2.17 as a third reference (all npm latest that day), Node 20.19.2, esbuild 0.28.2 node ESM minify + gzip -9.
- Fixture: a copied Jest/Vitest test project, 7,782 files including its node_modules, plus a .gitignore listing node_modules, coverage and *.log. Two jobs: a recursive *.js match across everything (3,736 hits) and the same match with node_modules excluded (481 hits). Every library returned identical counts.
- Timing: async API, one first call recorded separately, then the median of 31 calls, in two processes with the order reversed. A second fixture with only 400 source files checked whether the gitignore cost was a fixed overhead. Scripts and JSON in /workspace/bench-fast-glob-globby/.
- Not tested: cold disk cache (every run after the first is page-cache warm), Windows paths, network drives, monorepos with dozens of nested .gitignore files, the stream APIs, and Node's own fs.glob, which needs Node 22.
How fast is fast-glob vs globby?
| Job (median of 31, ms) | Run 1 | Run 2 | Files |
|---|---|---|---|
| fast-glob, whole tree | 16.27 | 15.66 | 3,736 |
| globby, whole tree | 20.04 | 18.70 | 3,736 |
| tinyglobby, whole tree | 15.67 | 14.75 | 3,736 |
| fast-glob, node_modules ignored | 0.72 | 0.73 | 481 |
| globby, node_modules ignored | 1.23 | 1.22 | 481 |
| globby, gitignore: true | 14.31 | 15.87 | 481 |
| tinyglobby, node_modules ignored | 0.87 | 1.21 | 481 |
globby calls fast-glob under the hood, so on plain globbing it adds a steady 3–4 ms on the big walk and about half a millisecond on the small one. The gitignore row is the real decision. On the 400-file second fixture the numbers were 1.04 ms for fast-glob, 1.59 ms for plain globby and 12.62 ms with gitignore: true, so on this box it behaves like a fixed cost of roughly 11–13 ms per call, not something that grows with your repo. I did not profile where that time goes. For a build script that globs once, 14 ms is noise; for a watcher or a dev-server plugin that re-globs on every change, it adds up. If part of your tooling is Python, recursive file listing there is measured in glob vs rglob vs os.walk, and the raw directory-read cost underneath any glob is measured in scandir vs listdir.
What does each one cost to install and bundle?
| Package | Minified | gzip | Input files | node_modules on disk |
|---|---|---|---|---|
| fast-glob 3.3.3 | 82,800 B | 26,147 B | 74 | 1,304 KB |
| globby 16.2.4 | 115,345 B | 37,736 B | 83 | 1,684 KB |
| tinyglobby 0.2.17 | 37,763 B | 13,568 B | 9 | 276 KB |
globby includes fast-glob as a dependency, so it is always the bigger of the two: about 11.6 KB more gzip, the ignore parser, the .gitignore handling and a few small helpers. If you ship a bundled CLI, the gap is the same kind I measured for process runners in tinyexec vs execa, and you can lock it in CI the way size-limit vs bundlesize describes. tinyglobby was the smallest by far and kept pace with fast-glob in both jobs here.
Do they return the same files?
| Check | fast-glob 3.3.3 | globby 16.2.4 |
|---|---|---|
| Directory name as the pattern (src) | 0 files | 441 files (expands to everything inside) |
| src/debug.log with a .gitignore *.log rule | returned | dropped with gitignore: true |
| Dotfiles by default | skipped | skipped |
| Negative pattern in the array | 420 files | 420 files |
| Module format / Node engines | CommonJS, Node 8.6+ | ESM only, Node 20+ |
Two rows matter in practice. globby's expandDirectories turns a bare folder name into its contents, so CLI arguments like src just work; with fast-glob you get an empty array and have to append the recursive pattern yourself. And globby is ESM only, which forces a dynamic import in an older CommonJS build script. If you are compiling that script anyway, the choice between bundlers in tsup vs unbuild handles the ESM switch. Path joining around the results is covered in pathe vs node:path, and if you are globbing to find dead files, Knip vs depcheck already does that walk for you.
Which one should you pick?
| Situation | My pick |
|---|---|
| Build script with a known ignore list | fast-glob |
| CommonJS code or Node below 20 | fast-glob |
| CLI that takes folder names and must respect .gitignore | globby |
| Hot path (watcher, dev plugin) that re-globs often | fast-glob or tinyglobby, ignore list written out |
| Smallest bundle for a published tool | tinyglobby |
Bottom line: on this box fast-glob 3.3.3 matched 481 files in about 0.72 ms, globby 16.2.4 in about 1.22 ms, and globby with gitignore: true in 14–16 ms, at a 37.7 KB versus 26.1 KB gzip bundle. Who should not switch to globby: hot paths and CommonJS projects that only need a fixed ignore list. Who should not stay on raw fast-glob: user-facing CLIs where people expect .gitignore and folder arguments to behave like git does.
How this was made: I installed all three packages on the ShopperCove box, copied a real test project as the fixture, ran each glob 31 times in two order-reversed processes, bundled each package with esbuild, and checked directory, dotfile and .gitignore behavior on the same tree. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/mrmlnc/fast-glob
- https://github.com/sindresorhus/globby
- https://github.com/SuperchupuDev/tinyglobby
- https://www.npmjs.com/package/fast-glob
- https://www.npmjs.com/package/globby
- https://esbuild.github.io/
Related
- https://www.shoppercove.com/blog/glob-vs-rglob-vs-walk-localhost-lab
- https://www.shoppercove.com/blog/scandir-vs-listdir-localhost-lab
- https://www.shoppercove.com/blog/tinyexec-vs-execa
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/tsup-vs-unbuild
- https://www.shoppercove.com/blog/pathe-vs-node-path
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/fnmatch-vs-re-localhost-lab
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~03:00 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). fast-glob 3.3.3 vs globby 16.2.4, tinyglobby 0.2.17 reference; esbuild 0.28.2 node ESM minify + gzip -9. Fixture: copied Jest/Vitest project, 7,782 files incl node_modules, .gitignore (node_modules, coverage, *.log). Recursive .js match, async API, median of 31, order-reversed (ms): whole tree 3,736 files fast-glob 16.27/15.66, globby 20.04/18.70, tinyglobby 15.67/14.75; node_modules ignored 481 files 0.72/0.73 vs 1.23/1.22 (tinyglobby 0.87/1.21); globby gitignore: true 14.31/15.87. 400-file fixture: 1.04 / 1.59 / 12.62 ms (gitignore). Bundle 82,800/26,147 B (74 inputs) vs 115,345/37,736 B (83); tinyglobby 37,763/13,568 B. node_modules on disk 1,304 KB vs 1,684 KB vs 276 KB. Behavior: globby expands a bare dir arg (src -> 441 files), fast-glob returns 0; gitignore: true drops *.log; both skip dotfiles; globby ESM-only Node 20+, fast-glob CJS Node 8.6+. Not tested: cold disk cache, Windows, network drives, many nested .gitignore files, streams, Node 22 fs.glob. No affiliate.