Plate 33
chokidar vs @parcel/watcher: 603 ms vs 61 ms to Ready
Aditya Challa8 min read
I pointed both watchers at the same 7,800-file repo copy on this box: @parcel/watcher 2.6.0 was ready in about 61 ms holding 1,319 inotify watches, while chokidar 5.0.0 took about 603 ms and held 9,119. Flip to latency and the result reverses: chokidar reported a single saved file in about 1.3 ms, @parcel/watcher in about 50.7 ms, because it batches events on purpose.
Short answer: pick @parcel/watcher for big trees, monorepos and anything that must survive restarts, and pick chokidar for small, scoped watches where you want per-event callbacks and a pure-JS install. 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:25 IST. fs.inotify.max_user_watches on this box is 127,940; Watchman is not installed, so @parcel/watcher used its inotify backend.
- Versions: chokidar 5.0.0 and @parcel/watcher 2.6.0 (npm latest that day), Node 20.19.2, esbuild 0.28.2 for bundle sizes.
- Fixture: a copied Jest/Vitest test project, 7,800 files in 1,318 directories including node_modules. Outside node_modules it is 505 files in 33 directories. Two modes: node_modules ignored (chokidar with an ignored function, @parcel/watcher with ignore set to node_modules) and the whole tree.
- Per run, in a fresh Node process: time from the watch call to ready, inotify watches counted from /proc/self/fdinfo, RSS after ready, 30 single-file writes timed to their event, a burst of 1,000 new files, an editor-style atomic save (write a temp file, rename it over the original) and a recursive delete. Five rounds per mode with the library order alternated; I report medians. Scripts and JSON are in /workspace/bench-chokidar-parcel-watcher/.
- Not tested: macOS FSEvents, Windows, the Watchman backend, polling mode, network or Docker bind mounts, chokidar 3.6 (the version Vite still ships), and trees large enough to hit the inotify limit.
How fast does each watcher get ready?
| Median of 5 fresh runs | chokidar 5.0.0 | @parcel/watcher 2.6.0 |
|---|---|---|
| Ready, node_modules ignored | 54.4 ms (52.6–80.3) | 23.0 ms (22.6–31.7) |
| Ready, whole tree | 603.4 ms (586.1–767.0) | 60.7 ms (55.4–66.0) |
| inotify watches, node_modules ignored | 539 | 34 |
| inotify watches, whole tree | 9,119 | 1,319 |
| RSS after ready, node_modules ignored | 67.3 MB | 57.9 MB |
| RSS after ready, whole tree | 138.7 MB | 61.7 MB |
The watch counts explain most of the table. chokidar's numbers line up with one inotify watch per file plus one per directory (505 + 33 + 1 = 539 and 7,800 + 1,318 + 1 = 9,119), while @parcel/watcher holds one per directory. An empty Node 20 process on this box sits at about 51.0 MB RSS, so on the whole tree chokidar added roughly 88 MB against roughly 11 MB. The 127,940-watch limit here would fit about 14 chokidar watchers on this repo with node_modules included, and chokidar's own README lists ENOSPC as the error you get when that runs out. The file-descriptor cousin of that failure, EMFILE from a low soft limit, is measured in ulimit soft vs hard. The initial crawl itself is a directory walk; the raw cost of reading directory entries is what scandir vs listdir times.
How quickly do change events arrive?
| Median of 5 runs | chokidar 5.0.0 | @parcel/watcher 2.6.0 |
|---|---|---|
| One file write to its event, median of 30 (ignored mode) | 1.30 ms | 50.65 ms |
| Same, p90 | 1.72 ms | 50.79 ms |
| One file write, whole tree | 1.37 ms | 50.66 ms |
| 1,000 new files: all delivered? | 1,000 of 1,000 | 1,000 of 1,000 |
| 1,000 new files: last event after first write | 92.3 ms | 84.9 ms |
The 50 ms floor is a design choice: the @parcel/watcher README says events are throttled and coalesced in C++ and handed to JavaScript as one batch, so the JS thread is not flooded during a git checkout or npm install. That shows up in the burst row, where it finished slightly ahead even though every single event is slower. If you are wrapping either watcher in your own debounce before a rebuild, the extra 50 ms is usually already inside your window; the helper sizes are compared in perfect-debounce vs lodash. For the kernel side of waiting on many descriptors, epoll vs select has a Python toy that shows why per-wait cost stays flat.
Do they report the same events?
| Action | chokidar 5.0.0 | @parcel/watcher 2.6.0 |
|---|---|---|
| Atomic save: write atomic.txt.tmp, rename over atomic.txt | change atomic.txt | create atomic.txt.tmp, create atomic.txt, delete atomic.txt.tmp |
| Recursive delete of a folder with 1,031 files | 1,031 unlink + 1 unlinkDir | 1,032 delete |
| Ignore option | function, regex or exact path (globs removed in v4) | paths or glob patterns (parsed with picomatch) |
| Events missed while the process was down | not available | getEventsSince with a snapshot file |
| Module format / Node | ESM only, Node 20.19+ | CommonJS wrapper plus native addon |
The atomic save row is the one that bites in practice. Editors that save by writing a temp file and renaming it over the original look like a plain change to chokidar, because its atomic option merges the unlink and add. @parcel/watcher shows the raw sequence, and the overwrite arrives as create rather than update, so a handler that only listens for update will miss it. Why editors save that way is covered in atomic rename vs overwrite. The snapshot feature is the part chokidar has no answer for: writeSnapshot took a median 39.3 ms and wrote a 1,135,617-byte file for this tree, and getEventsSince took a median 50.5 ms and returned the one file I created while nothing was watching.
What does each one cost to install?
| Package | Packages installed | node_modules | Bundled JS (min / gzip) |
|---|---|---|---|
| chokidar 5.0.0 | 2 (chokidar, readdirp) | 152 KB | 22,565 B / 8,325 B, 4 inputs |
| @parcel/watcher 2.6.0 | 8 incl. glibc and musl binaries | 2,008 KB | 31,897 B / 11,682 B, 15 inputs, plus a 523,152-byte watcher.node |
The native addon cannot go into a JS bundle, so a bundled CLI that uses @parcel/watcher still needs the platform package installed next to it. If you track the size of your published tool, wire the JS part into size-limit vs bundlesize and list the binary separately.
Which one should you pick?
| Situation | My pick |
|---|---|
| Watching a large tree or a whole monorepo | @parcel/watcher |
| Need changes made while your tool was not running | @parcel/watcher (snapshots) |
| Small, scoped watch where each save should fire within a few ms | chokidar |
| Pure-JS install, no native binaries in CI or containers | chokidar |
| Watching config files next to a dev server | chokidar, which is what Vite uses |
For context, Vite's server.watch docs say its options are passed to chokidar 3.6 and that it skips node_modules and .git by default, which is the ignored mode in my tables; the dev-server comparison itself is in Vite vs Parcel, and Parcel is the project that maintains @parcel/watcher.
Bottom line: on this box, with the whole 7,800-file tree, @parcel/watcher 2.6.0 was ready in 60.7 ms with 1,319 watches and 61.7 MB RSS, against 603.4 ms, 9,119 watches and 138.7 MB for chokidar 5.0.0. chokidar answered a single save in 1.3 ms against 50.7 ms. Who should not switch to @parcel/watcher: small watchers where per-event speed and a pure-JS install matter more than memory. Who should not stay on chokidar: tools that watch big trees, run many watchers per machine, or need to know what changed while they were off.
How this was made: I installed both packages on the ShopperCove box, copied a real test project as the fixture, ran each watcher five times per mode in fresh processes with the order alternated, counted inotify watches from /proc, and checked atomic-save, delete and snapshot 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/paulmillr/chokidar
- https://github.com/parcel-bundler/watcher
- https://www.npmjs.com/package/chokidar
- https://www.npmjs.com/package/@parcel/watcher
- https://vite.dev/config/server-options
- https://man7.org/linux/man-pages/man7/inotify.7.html
Related
- https://www.shoppercove.com/blog/ulimit-soft-vs-hard-emfile-lab
- https://www.shoppercove.com/blog/scandir-vs-listdir-localhost-lab
- https://www.shoppercove.com/blog/perfect-debounce-vs-lodash
- https://www.shoppercove.com/blog/epoll-vs-select-fd-setsize-localhost-lab
- https://www.shoppercove.com/blog/atomic-rename-vs-overwrite-localhost-lab
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/vite-vs-parcel
- https://www.shoppercove.com/blog/tsup-vs-unbuild
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~03:25 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2, max_user_watches 127,940, no Watchman so @parcel/watcher used inotify). chokidar 5.0.0 vs @parcel/watcher 2.6.0. Fixture: copied Jest/Vitest project, 7,800 files / 1,318 dirs (505 files / 33 dirs outside node_modules). Fresh process per run, 5 rounds per mode, order alternated, medians: whole tree ready 603.4 ms (586.1-767.0) vs 60.7 (55.4-66.0); inotify watches 9,119 vs 1,319 (chokidar = one per file + dir); RSS 138.7 vs 61.7 MB (empty Node 51.0 MB). node_modules ignored: 54.4 vs 23.0 ms, 539 vs 34 watches, 67.3 vs 57.9 MB. Single write to event median of 30: 1.30 ms (p90 1.72) vs 50.65 ms (p90 50.79; README says events are throttled/coalesced in C++). Burst 1,000 files: all delivered, last event 92.3 vs 84.9 ms. Atomic save (tmp + rename): chokidar 1 change; parcel create tmp, create target, delete tmp. Recursive delete 1,031 files: 1,031 unlink + 1 unlinkDir vs 1,032 delete. writeSnapshot 39.3 ms (1,135,617 B), getEventsSince 50.5 ms, caught offline create. Install 2 pkgs 152 KB vs 8 pkgs 2,008 KB (523,152 B watcher.node); esbuild JS gzip 8,325 B vs 11,682 B. Not tested: macOS FSEvents, Windows, Watchman, polling, Docker/network mounts, chokidar 3.6 (Vite), inotify-limit exhaustion. No affiliate.