8 min read
picomatch is about 2x faster only for list filtering: on 7,800 paths, micromatch 4.0.8 took a median 3.73 ms to return 481 .js files with an include plus a node_modules negation, while two picomatch 4.0.7 matchers in Array.filter took 1.81 ms, and precompiled matchers ran at the same speed. micromatch also installs 6 packages against picomatch's 1, and npm audit flags its braces dependency (GHSA-vfj7-8cjw-p6xm) 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.
| 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.
| 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.
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.
| 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.
| 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.
A high-severity advisory for braces through 3.0.3: deeply nested brace patterns can overflow the call stack, and no patched version is listed. micromatch 4.0.8 pulls braces 3.0.3; picomatch does not.
Only in the list call (1.81 ms vs 3.73 ms). Once a pattern is compiled they match at the same speed, because micromatch's matcher is a picomatch regex underneath.
When you need its list helpers (not, some, every, matchKeys) or real brace expansion. For browser bundles and hot paths that only need yes/no matching, use picomatch.
Lab evidence
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.
Explore the product
Untested by us, and no traffic result is guaranteed. Check the live offer and refund terms first.