Plate 15
micromatch vs picomatch: 3.7 ms vs 1.8 ms on 7,800 Paths
Aditya Challa7 min read
I filtered the same 7,800 repo paths with both libraries on this box: micromatch 4.0.8 took a median 3.73 ms to return the 481 .js files outside node_modules from an include plus a negation, while two picomatch 4.0.7 matchers inside Array.filter returned the same 481 in 1.81 ms. micromatch also installs 6 packages against picomatch's 1, and npm audit flags its braces dependency as high severity with no patched version listed.
Short answer: use picomatch directly when you control the patterns and want the smallest, browser-safe matcher, and keep micromatch when you need its list helpers or real brace expansion. Either way, compile each pattern once. 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:30 IST.
- Versions: micromatch 4.0.8 and picomatch 4.0.7 (npm latest that day), Node 20.19.2, esbuild 0.28.2 for bundle sizes. Note that micromatch 4.0.8 does not use picomatch 4; its lockfile resolved picomatch 2.3.2 plus braces 3.0.3, fill-range, to-regex-range and is-number.
- Input: 7,800 relative file paths listed from a copied Jest/Vitest project, node_modules included. Patterns, in words: the recursive any-depth .js pattern; a src-only pattern with a js/ts/mjs brace list; an extglob for test or spec files; a numeric range from 1 to 20; and an any-depth node_modules negation.
- Timing: each job warmed once, then the median of 21 runs (11 for the compile-per-call jobs), in 6 fresh processes with the library order alternated. Results are the median of those 6. Scripts and JSON are in /workspace/bench-micromatch-picomatch/.
- Not tested: Windows paths and the windows option, real file-system globbing (that is fast-glob's job, which uses micromatch), Bun or Deno, and micromatch's own newer major if one ships.
Is picomatch faster than micromatch?
| Job on 7,800 paths (median, ms) | micromatch 4.0.8 | picomatch 4.0.7 | Matches |
|---|---|---|---|
| Precompiled matcher, recursive .js | 1.322 | 1.317 | 3,736 |
| Precompiled matcher, src brace list | 0.400 | 0.415 | 440 |
| Precompiled matcher, test/spec extglob | 1.395 | 1.413 | 53 |
| Filter with include + node_modules negation | 3.732 (3.50–4.58) | 1.812 (1.76–1.96) | 481 |
Once a pattern is compiled, the two are the same speed, which makes sense because micromatch's matcher is a picomatch regex underneath. The 2x gap is in the list call: micromatch(list, patterns) handles the negation and de-duplication for you, and that bookkeeping costs about 1.9 ms on this input. Writing the two matchers yourself and filtering is a three-line change.
The bigger cost is compiling inside a loop. Calling isMatch with a pattern string 1,000 times took 16.3 ms for the extglob with micromatch and 15.7 ms with picomatch, about 16 µs a call, while the precompiled matcher handled 7,800 paths in about 1.4 ms, roughly 0.18 µs a path. That is close to 90x, and it is the same for both libraries. It is the same compile-once lesson as lru_cache hit vs miss, and Python users will recognize the shape from fnmatch vs re.
What does each one cost to install and bundle?
| Package | Packages | node_modules | esbuild node (min / gzip) | Browser build |
|---|---|---|---|---|
| micromatch 4.0.8 | 6 | 360 KB | 41,479 B / 14,756 B, 18 inputs | failed: needs util and path |
| picomatch 4.0.7 | 1 | 132 KB | 24,518 B / 8,892 B, 7 inputs | built, same size |
The browser failure is the kind of thing pathe vs node:path is about: Node built-ins do not resolve in a browser bundle. If you ship a matcher to a web tool or an editor extension, picomatch avoids that, and you can hold the 5.9 KB gzip difference in place with size-limit vs bundlesize.
Is the braces advisory a real problem?
npm audit on a fresh micromatch install reported 2 high-severity findings, both from GHSA-vfj7-8cjw-p6xm: braces through 3.0.3 can overflow the call stack on deeply nested brace patterns, and the advisory lists no patched version. A fresh picomatch install reported 0. I checked what that means on this box with nested patterns of the form opening braces, a comma list, closing braces, each in a fresh process:
| Call (fresh process) | 6,003 chars | 8,003 chars | 9,983 chars |
|---|---|---|---|
| micromatch.braces() | ok | RangeError | RangeError |
| micromatch.braces() with expand | ok | ok | RangeError |
| micromatch.isMatch() | ok | ok | ok |
| micromatch(list, pattern) | ok | ok | ok |
| picomatch.isMatch() | ok | ok | ok |
braces rejects input over 10,000 characters with a SyntaxError, so the overflow window is just under that cap. In my runs it only fired through micromatch.braces(); the matching calls returned normally on the same strings. Separately, a 40,001-character nested pattern passed to isMatch crashed both libraries with a V8 regex out-of-memory abort (exit code 134), which no try/catch can stop. If patterns come from users, cap their length yourself. The broader habit of checking what a dependency pulls in is in the supply-chain checklist for small teams, and ncu vs npm outdated is how I watch for a patched braces release.
Do they match the same paths?
| Pattern and input | micromatch | picomatch |
|---|---|---|
| file-{1..3}.js vs file-2.js | true | true |
| f-{1..20}.js vs f-5.js and f-15.js | false, false | false, false |
| f-{1..20}.js vs f-0.js | true | true |
| f-{1..20}.js with expandRange via fill-range, vs f-15.js | true | true |
| a/{b,c}/d.js vs a/c/d.js | true | true |
| star vs .env, default / dot: true | false / true | false / true |
| src backslash a.js vs src/star.js on Linux | false | false |
| List helpers (not, some, every, contains, matchKeys, braces) | yes | no; returns a matcher, plus scan, parse, makeRe, test |
The surprise row: both libraries compile a multi-digit range like 1..20 to the character class 1-20, which matches only 0, 1 and 2. f-15.js and f-5.js silently fail, and the zero-padded 01..10 form failed too. Passing an expandRange function built on fill-range fixed it in both, as did expanding the list first with micromatch.braces and the expand option. If unused pattern lists are piling up in a repo, Knip vs depcheck finds the dead files they were written for.
Which one should you pick?
| Situation | My pick |
|---|---|
| Hot path, patterns known up front | picomatch, compiled once |
| Browser or editor-extension bundle | picomatch |
| Need not, some, every or matchKeys on lists | micromatch |
| Need real numeric range expansion | micromatch.braces with expand, or picomatch with expandRange |
| User-supplied patterns | either, with your own length cap |
Bottom line: on this box micromatch 4.0.8 filtered 7,800 paths in 3.73 ms against 1.81 ms for picomatch 4.0.7, at 14,756 B versus 8,892 B gzip and 6 packages versus 1. Precompiled matchers were the same speed, and compiling per call was about 90x slower in both. Who should not switch to picomatch: code that leans on micromatch's list helpers or brace expansion. Who should not stay on micromatch: browser bundles and hot paths that only need a yes/no match.
How this was made: I installed both packages on the ShopperCove box, listed a real test project's paths as input, timed matchers, per-call compiles and list filtering in 6 fresh processes with the order alternated, bundled each package with esbuild, ran npm audit, and tested range, dotfile and nested-brace behavior in separate processes. 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/micromatch/micromatch
- https://github.com/micromatch/picomatch
- https://github.com/micromatch/braces
- https://github.com/advisories/GHSA-vfj7-8cjw-p6xm
- https://github.com/jonschlinkert/fill-range
- https://www.npmjs.com/package/micromatch
- https://www.npmjs.com/package/picomatch
- https://esbuild.github.io/
Related
- https://www.shoppercove.com/blog/lru-cache-hit-miss-localhost-lab
- https://www.shoppercove.com/blog/fnmatch-vs-re-localhost-lab
- https://www.shoppercove.com/blog/pathe-vs-node-path
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/after-xz-practical-supply-chain-checklist-solo
- https://www.shoppercove.com/blog/ncu-vs-npm-outdated
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/path-glob-vs-fnmatch-localhost-lab
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~03:30 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). micromatch 4.0.8 (resolves picomatch 2.3.2 + braces 3.0.3 + fill-range + to-regex-range + is-number) vs picomatch 4.0.7; esbuild 0.28.2. Input: 7,800 relative paths from a copied Jest/Vitest project. Median of 21 runs (11 for per-call compile) x 6 fresh processes, order alternated (ms on 7,800 paths): precompiled recursive .js 1.322 vs 1.317 (3,736 hits), src brace list 0.400 vs 0.415 (440), test/spec extglob 1.395 vs 1.413 (53); micromatch(list, [include, negation]) 3.732 (3.50-4.58) vs two picomatch matchers + Array.filter 1.812 (1.76-1.96), 481 hits each. isMatch with pattern string x1,000: extglob 16.3 vs 15.7 ms (~90x precompiled). Bundle node 41,479/14,756 B (18 inputs) vs 24,518/8,892 B (7); micromatch browser build fails on util/path. Install 6 pkgs 360 KB vs 1 pkg 132 KB; npm audit 2 high (GHSA-vfj7-8cjw-p6xm, braces <=3.0.3, no patch) vs 0. Fresh-process nested braces: micromatch.braces RangeError at 8,003 chars, expand at 9,983; isMatch/list/picomatch ok to 9,983; >10,000 chars SyntaxError; 40,001-char pattern into isMatch aborted both (V8 regex OOM, exit 134). Range {1..20} compiles to [1-20] in both (f-15.js false) unless expandRange/fill-range. Not tested: Windows paths/windows option, fs globbing, Bun/Deno. No affiliate.
Related links
Plate 08
fast-glob vs globby: 0.7 ms vs 14 ms With gitignore
5 Oct 2026
Plate 55
Klona vs structuredClone: 1.4M vs 225k Clones per Second
5 Oct 2026
Plate 12
Yarn Berry vs pnpm: 1.3s vs 0.24s Install
Yarn 4.9.2 (Berry PnP + node-modules linker) vs pnpm 12.9.1 on one 22-dependency React+Vite app. Isolated caches (Yarn enableGlobalCache false; pnpm --store-dir). Warm medians (5): Yarn PnP 1.307 s, Yarn nm 2.471 s, pnpm 0.240 s. Lock+empty medians 1.656 / 2.789 / 0.725 s. Clean disk: Yarn PnP 246 MB; Yarn nm modules 189 MB; pnpm hardlink project 167 MB. Fixture /workspace/bench-yarn-pnpm/. Distinct from pnpm-vs-npm-vs-bun (no Yarn).
5 Oct 2026