Skip to Content
GuideCompetitor Evidence

Competitor benchmark evidence

This compares overlapping workloads adapted from fdir and tinyglobby’s own benchmarks against the measured pre-release Vooya public entry. It is a baseline for choosing optimizations. These reports measure the reviewed release build, including the documented glob compatibility corrections.

Findings

  • On the real typescript-eslint source tree, native sync glob wins the three full-tree matching workloads against the other tested synchronous APIs.
  • Native async glob beats Node’s built-in glob but loses to the fastest tested fdir/tinyglobby implementation. These two facts must not be conflated.
  • Default native recursive readdir loses to fdir in both modes on that tree. A follow-up concurrency sweep below reduces native async time by about 61–62% with four workers and sharply reduces CPU cost, bringing it close to fdir.
  • packages/*/tsconfig.json uses Vooya’s Node fallback in both modes. Its results demonstrate compatibility/routing overhead, not Rust acceleration.
  • Tiny and wide/flat trees remain important counterexamples. A single deep-tree win is not evidence of a universally faster crawler. Close timings should be treated as comparable until repeated in the target application.

Provenance and equivalent work

The pinned upstream scripts, full MIT notices, modifications, fixture setup and reproduction commands are in test/performance/competitors/SOURCES.md .

The upstream tinyglobby script uses a typescript-eslint clone. We pin it to 338fad98047cf80b88e2090c85dab9def3e2e782 and use the source archive without installing dependencies. The benchmark verifies a manifest of names, types and sizes before timing. Original benchmark defaults sometimes return files only, all entries, or Dirents; this adaptation returns relative strings for matching files AND directories throughout, with no order requirement. Glob excludes dot entries; plain readdir includes them. Gitignore is disabled. Symlinks/special files are rejected by fixture validation. This is a deliberately shared subset, not a complete library conformance claim.

A directory named folder.js, hidden entries, duplicates and omitted entries are covered by adapter tests. An independent fixture manifest selects exact expected paths for the simple patterns. Every warmup and sample is checked. Only slash spelling/trailing directory slash are normalized outside timing. If an application needs canonicalized paths, its conversion cost must be measured separately.

Method

  • Apple M4 Pro, 14 logical CPUs, macOS arm64. Node 22.22.0 and Node 24.21.0.
  • Release native build; fdir 6.5.0, picomatch 4.0.7, fast-glob 3.3.3, tinyglobby 0.2.17, glob 13.0.6. The pnpm lockfile fixes transitive versions.
  • Two warmups, ten samples per library/case. Tables show median milliseconds. Engine order rotates per sample and per case; raw evidence also includes p90, CPU samples and RSS deltas. No speed thresholds are asserted in CI.
  • Each call constructs its crawler/matchers and fully materializes the result. Sync timings do not include an artificial await; async Node glob is fully collected. Caller-managed persistent glob caches are not exercised.
  • Warm filesystem and library-internal caches; setup, GC and validation excluded. These are repeat-call measurements, not cold storage or process startup.
  • Library defaults: native glob uses four workers, recursive readdir the default Rayon pool. UV_THREADPOOL_SIZE and RAYON_NUM_THREADS were unset. The table is a comparison of these adapter/default configurations, not every library’s best possible manually tuned configuration.
  • RSS is immediately-after minus immediately-before, not peak memory. Zero does not mean no allocation. All libraries are loaded into the same process; process CPU time includes native worker threads.
  • Vooya routing is verified by untimed instrumentation restored before timing. Other than choosing the public API and documented options, the runtime code is unchanged. Source, lockfile and native binary hashes are included in each report.
  • Not measured: peak memory, event-loop delay, startup, streaming first result, cold-cache I/O, filesystem mutation/error races, metadata-heavy scan, Rust-only libraries or Windows/Linux speed. Platform CI tests adapter correctness only.

Fixtures

FixtureFilesDirectories including rootPurpose
tiny137Small-input overhead plus hidden/matching-directory sentinels
typescript-eslint7,5772,448Pinned upstream real source repository
wide-2000020,0057Flat fan-out plus the same sentinels; supplemental stress shape

Full results

all = **/*; configs = packages/*/tsconfig.json; javascript = **/*.js; multi = four patterns for .ts, .tsx, .js, .md; readdir = recursive listing without glob. The multi-pattern and synthetic cases are Vooya supplements. A dash means the library was not entered in the plain-readdir comparison.

v22.22.0 — tiny

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync0.4720.2710.4060.3240.6073.203native
allsync0.2810.2010.2470.2200.3413.158native
configsasync0.2380.2810.2880.2140.4430.280node-fallback
configssync0.1820.2080.2210.1890.3840.182node-fallback
javascriptasync0.3940.2840.3680.2990.5432.421native
javascriptsync0.2860.2290.2290.2270.4803.186native
multiasync0.5170.3400.3650.3030.5853.182native
multisync0.4130.2910.2950.2580.4183.215native
readdirasync0.3500.238———0.247native
readdirsync0.1890.165———0.204native

v22.22.0 — typescript-eslint

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync106.75517.44623.16819.69126.59324.585native
allsync73.42039.26340.89740.85953.77325.430native
configsasync0.62818.4990.5140.4970.7260.656node-fallback
configssync0.32338.2640.7330.7120.6700.344node-fallback
javascriptasync93.72119.44423.09017.31121.61924.541native
javascriptsync63.98539.12940.91940.51046.17724.500native
multiasync163.53619.85223.15320.04626.26224.782native
multisync128.62243.26044.98544.60151.15725.824native
readdirasync97.46119.770———46.342native
readdirsync71.30935.630———47.748native

v22.22.0 — wide-20000

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync43.31815.29514.58315.25161.01218.667native
allsync34.41114.54014.41115.07156.49518.798native
configsasync0.36414.5160.4260.3670.5010.397node-fallback
configssync0.19014.0540.2600.2470.4430.200node-fallback
javascriptasync34.35014.45313.54214.40655.54218.432native
javascriptsync30.31414.37913.55814.65147.25318.282native
multiasync97.54517.35515.08317.17364.81119.092native
multisync85.38217.03914.85117.19159.09318.694native
readdirasync68.82013.439———14.010native
readdirsync68.87213.328———13.848native

v24.21.0 — tiny

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync0.5070.3410.4140.4090.6101.817native
allsync0.2960.2090.2730.2340.3721.760native
configsasync0.2590.3080.3080.2410.4250.297node-fallback
configssync0.1710.2270.2340.1880.3780.179node-fallback
javascriptasync0.4780.3210.4010.3480.5153.245native
javascriptsync0.2910.2210.2170.2430.4691.825native
multiasync0.5830.4170.4150.4050.5631.817native
multisync0.3790.2980.2990.2690.4261.838native
readdirasync0.3560.278———0.316native
readdirsync0.2250.178———0.216native

v24.21.0 — typescript-eslint

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync107.72320.35221.63019.82926.30424.306native
allsync69.12242.57344.52443.50959.83024.008native
configsasync0.58920.6130.5340.4850.6700.655node-fallback
configssync0.35140.7180.8020.7280.6070.362node-fallback
javascriptasync96.43720.24620.92920.42821.86924.635native
javascriptsync60.62641.76444.93243.42053.40324.057native
multiasync159.61419.79522.03318.68127.70624.311native
multisync119.04146.06748.61047.09559.86122.810native
readdirasync99.83121.800———53.377native
readdirsync71.26938.546———54.092native

v24.21.0 — wide-20000

TaskModeNodefdirfast-globtinyglobbyglobVooyaVooya route
allasync39.98315.96115.25115.26559.46718.555native
allsync29.02315.72214.27415.36554.48919.427native
configsasync0.33915.1720.4260.3810.5470.386node-fallback
configssync0.19814.2700.2730.2550.4270.213node-fallback
javascriptasync28.42315.37113.33115.17752.14918.264native
javascriptsync25.25615.19113.10415.02545.89618.307native
multiasync91.30418.15015.43817.36962.11218.578native
multisync79.71117.90314.76517.44857.36819.060native
readdirasync66.31013.446———14.105native
readdirsync65.59713.157———17.027native

Focused readdir concurrency diagnostic

The default real-tree readdir run consumed substantially more CPU than fdir. This follow-up keeps the exact recursive relative-path result and public async entry, varies only Vooya’s documented concurrency option, and remeasures Node and fdir alongside it. Same two warmups, ten samples, rotating order and validation. These measurements are separate runs; compare baselines within each table.

NodeImplementation / workersMedian msProcess CPU ms
v22.22.0node102.364109.705
v22.22.0fdir19.34398.165
v22.22.0Vooya default47.521620.487
v22.22.0Vooya 138.86439.740
v22.22.0Vooya 225.57675.954
v22.22.0Vooya 418.40589.031
v22.22.0Vooya 829.496256.171
v24.21.0node99.272106.097
v24.21.0fdir20.25192.465
v24.21.0Vooya default52.731637.293
v24.21.0Vooya 141.15442.151
v24.21.0Vooya 227.82782.733
v24.21.0Vooya 420.17497.659
v24.21.0Vooya 833.961295.914

Four workers reduce wall time by about 61–62% and process CPU time by about 85–86% versus the native default on this machine and fixture. Their 18.405/20.174 ms medians are close to fdir’s 19.343/20.251 ms on Node 22/24; these small differences do not establish a repeatable lead. Eight workers regress relative to four. The default’s high CPU cost supports investigating scheduling/oversubscription, but does not prove which internal operation dominates or justify changing defaults for all machines. No default was changed.

What to investigate next

  1. Profile recursive readdir scheduling, default worker count and path/result allocation. The concurrency sweep identifies a concrete low-scope experiment.
  2. Profile async glob against fdir/tinyglobby, separating traversal, matching and N-API result conversion. The large Node baseline gap hides the smaller, more relevant competitor gap. Sync wins do not establish async competitiveness.
  3. Test targeted subtree planning. fdir’s generic root crawler in this adapter scans the full source tree for configs, while the other glob planners can constrain the search. Manual fdir root/depth tuning is not included, so that row is not a claim that fdir cannot be used efficiently for a known layout.
  4. Keep the tiny and flat cases as counterweights. The full API scan comparison now covers metadata-heavy work; extend it to larger real repositories and additional platforms before expanding performance recommendations.

Reproduce and inspect raw data

Prepare the pinned fixture using SOURCES.md, then:

pnpm build pnpm perf:competitors --fixture .perf/competitors/typescript-eslint --output .perf/competitors-node22.json npm exec --yes --package=node@24.21.0 -- node --expose-gc scripts/benchmark-competitors.mjs --fixture .perf/competitors/typescript-eslint --output .perf/competitors-node24.json node --expose-gc scripts/benchmark-readdir-concurrency.mjs .perf/competitors/typescript-eslint .perf/competitors-readdir-concurrency-node22.json npm exec --yes --package=node@24.21.0 -- node --expose-gc scripts/benchmark-readdir-concurrency.mjs .perf/competitors/typescript-eslint .perf/competitors-readdir-concurrency-node24.json

Use Node 22.22.0 for the unprefixed commands. Run each command separately with no concurrent builds/tests. pnpm test:competitor-contract validates adapters without running timing benchmarks, and is wired into the platform CI test matrix.

Last updated on