chokidar vs gaze vs node-watch vs nodemon vs watch
Node.js 文件监听方案选型与架构对比
chokidargazenode-watchnodemonwatch类似的npm包:

Node.js 文件监听方案选型与架构对比

chokidargazenode-watchnodemonwatch 都是 Node.js 生态中用于监听文件系统变化的工具,但它们的定位和适用场景差异巨大。chokidar 是目前社区事实上的标准跨平台监听库,提供了稳定且一致的事件接口。gazewatch 是早期的监听方案,目前大多已停止维护或不再推荐用于新项目。node-watch 是对原生 fs.watch 的轻量级封装,适合简单场景。nodemon 则是一个专门用于开发环境中自动重启 Node.js 应用的工具,虽然底层涉及文件监听,但其核心目的是进程管理而非通用文件监控。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
chokidar012,13182.1 kB446 个月前MIT
gaze01,154-688 年前MIT
node-watch034226.1 kB83 年前MIT
nodemon026,683219 kB124 个月前MIT
watch01,279-599 年前Apache-2.0

Node.js 文件监听方案选型与架构对比

在 Node.js 开发中,监听文件变化是构建工具、热重载(HMR)和自动化任务的核心需求。虽然表面上看这些包都在做同一件事,但它们的底层实现、稳定性和适用场景截然不同。作为架构师,我们需要清楚区分“通用监听库”与“开发工具”,并识别哪些是历史遗留产物。

🏗️ 核心定位:通用库 vs 开发工具

首先必须明确,nodemon 与其他四个包不在同一个类别。

chokidargazenode-watchwatch 是通用文件监听库。

  • 它们暴露文件事件(add, change, unlink)。
  • 你可以用它们触发构建、同步文件或更新缓存。
  • 它们是底层基础设施。

nodemon 是开发辅助工具。

  • 它的核心目的是监测代码变化并重启 Node 进程。
  • 虽然它底层也监听文件,但它不希望你直接处理文件事件。
  • 它是开箱即用的解决方案,而非构建模块。
// nodemon: 专注于重启进程,而非暴露文件事件
const nodemon = require('nodemon');

nodemon({
  script: 'app.js',
  watch: ['src/'],
  ext: 'js,json'
});

nodemon.on('restart', () => {
  console.log('应用已重启');
});
// 注意:它没有提供类似 'fileChanged' 的通用事件供你处理业务逻辑
// chokidar: 专注于暴露文件事件,供你编写业务逻辑
const chokidar = require('chokidar');

const watcher = chokidar.watch('src/', {
  ignored: /node_modules/,
  persistent: true
});

watcher.on('change', path => {
  console.log(`文件 ${path} 已修改,触发构建`);
  // 这里可以执行任意逻辑,如打包、部署等
});

🛠️ API 设计与事件处理

不同的库在事件命名和参数传递上有显著差异,这直接影响代码的可读性和迁移成本。

chokidar 使用语义清晰的事件名(add, addDir, change, unlink, unlinkDir)。

  • 它区分文件和目录的变化,这对于递归监听非常重要。
  • 回调函数通常只接收路径字符串,保持简洁。
// chokidar: 语义化的事件区分
watcher.on('add', path => console.log(`新建文件:${path}`));
watcher.on('unlink', path => console.log(`删除文件:${path}`));
watcher.on('change', path => console.log(`文件变动:${path}`));

node-watch 的事件类型较为简单(update, remove)。

  • 它通过事件对象传递更多信息(如文件名)。
  • 适合不需要区分目录和文件的简单场景。
// node-watch: 通过事件对象传递信息
watch('src/', { recursive: true }, function(evt, name) {
  console.log('%s %s', evt, name);
  // evt 可能是 'update' 或 'remove'
});

gaze 使用 changed, added, deleted 事件。

  • 它支持通配符匹配,但 API 风格较旧(基于回调而非 Promise 友好)。
  • 事件参数有时包含文件统计信息,但一致性不如 chokidar
// gaze: 旧式回调风格
const gaze = require('gaze');

gaze(['**/*.js'], function(err, watcher) {
  watcher.on('changed', function(filepath) {
    console.log('文件被修改', filepath);
  });
  watcher.on('added', function(filepath) {
    console.log('文件被添加', filepath);
  });
});

watch (mikeal/watch) 的 API 非常原始。

  • 它主要依赖 watch.watchTree 方法。
  • 回调函数接收当前文件、当前统计信息和上一个统计信息。
  • 缺乏现代异步流的支持,处理起来较为繁琐。
// watch: 原始的文件树监听
const watch = require('watch');

watch.watchTree('src/', function(f, curr, prev) {
  if (typeof f == "object" && prev === null && curr === null) {
    // 遍历完成
  } else if (prev === null) {
    console.log('新文件', f);
  } else {
    console.log('文件修改', f);
  }
});

🐢 性能与稳定性:原生封装 vs 轮询

文件监听的性能直接影响开发体验,特别是在大型单体仓库(Monorepo)中。

chokidar 优先使用系统原生的 fs.watch,并在检测到不可靠时自动降级到 fs.watchFile(轮询)。

  • 它内部处理了 macOS 上的已知问题(如事件重复触发)。
  • 支持 awaitWatcher 等机制确保事件处理完成。
  • 在大多数场景下,它是性能与稳定性的最佳平衡点。
// chokidar: 自动处理底层差异,支持持久化监听
const watcher = chokidar.watch('project/', {
  persistent: true,
  usePolling: false, // 默认优先使用原生事件
  interval: 100      // 仅在轮询模式下生效
});

node-watch 同样是对原生的封装,但策略更简单。

  • 它试图修复原生 fs.watch 的某些 bug,但功能集较小。
  • 在极端高频率文件变动下,可能会丢失事件或表现不稳定。
// node-watch: 简单的递归选项
watch('project/', { recursive: true }, function(evt, name) {
  // 在高负载下可能不如 chokidar 稳定
});

gazewatch 在旧版本 Node.js 上表现尚可,但在新版 V8 和文件系统变更下已显疲态。

  • 它们缺乏对现代文件系统事件(如原子写入)的优化处理。
  • 容易出现“事件风暴”,导致 CPU 占用过高。
// gaze: 缺乏现代优化,可能导致高 CPU 占用
gaze('**/*', function() {
  // 在大型项目中,回调触发频率难以控制
});

⚠️ 维护状态与风险提示

在架构选型中,库的维护状态是决定性因素。

chokidar 处于活跃维护状态。

  • 它是 Vite、Rollup、Webpack 等主流工具的依赖。
  • 社区支持强大,Bug 修复及时。
  • 推荐用于所有生产级项目。

nodemon 处于活跃维护状态。

  • 但请仅将其用于本地开发环境。
  • 不推荐将其逻辑集成到生产代码中。

gazewatch 基本处于停滞状态。

  • GitHub 仓库更新频率极低。
  • 存在未修复的 Issue,且不支持 Node.js 最新特性。
  • 明确建议:不要在新项目中使用。

node-watch 维护频率较低但相对稳定。

  • 适合轻量级脚本,但不适合复杂构建系统。
  • 如果未来需要扩展功能,迁移成本较高。
// 风险示例:使用已停止维护的库可能导致未来升级失败
// 不推荐
const watch = require('watch'); 

// 推荐
const chokidar = require('chokidar');

📊 总结对比表

特性chokidarnode-watchnodemongazewatch
定位通用监听库通用监听库开发重启工具通用监听库 (旧)通用监听库 (旧)
维护状态✅ 活跃⚠️ 低频✅ 活跃❌ 停滞❌ 停滞
跨平台稳定性高 (仅限重启)
API 现代性高 (Promise 友好)低 (回调)低 (回调)
适用场景构建工具、生产监控简单脚本本地开发调试遗留项目维护遗留项目维护
是否推荐强烈推荐可选开发环境推荐不推荐不推荐

💡 架构师建议

在设计前端工程化体系时,文件监听模块应当是透明且可靠的。

  1. 默认选择 chokidar:无论是编写自定义 CLI、构建插件还是后端同步服务,chokidar 都是最安全的选择。它的 API 设计允许你轻松处理防抖(debounce)和批量处理,这对于避免构建风暴至关重要。

  2. 隔离 nodemon:将 nodemon 限制在 package.jsondevDependencies 和开发脚本中。不要让它渗透到你的核心业务逻辑或部署脚本里。

  3. 清理技术债务:如果代码库中还存在 gazewatch,请制定迁移计划。它们的 API 差异不大,替换为 chokidar 通常只需修改导入语句和事件名称,但能显著提升长期稳定性。

  4. 关注原子写入:在 Linux 和 macOS 上,编辑器保存文件通常是“写入临时文件 -> 重命名”的过程。chokidar 能较好地处理这种 unlink + add 的组合事件,而老旧库可能会将其误判为文件删除,导致监听失效。

选择正确的工具不仅仅是为了代码能跑,更是为了在系统规模扩大时,减少不可预知的运维成本。

如何选择: chokidar vs gaze vs node-watch vs nodemon vs watch

  • chokidar:

    如果你的项目需要跨平台稳定运行,且对文件变动的准确性要求较高,chokidar 是首选。它解决了原生 fs.watch 在不同操作系统上的不一致问题,支持忽略模式、批量事件处理等高级功能。大多数现代构建工具(如 Vite、Webpack)底层都依赖它。

  • gaze:

    仅当你维护非常老旧的项目且无法迁移时使用 gaze。该库已不再积极维护,功能已被 chokidar 等更现代的方案超越。在新架构中引入它会增加技术债务,建议尽快替换。

  • node-watch:

    如果你需要比原生 fs.watch 更好用但比 chokidar 更轻量的方案,可以选择 node-watch。它适合简单的脚本工具或对依赖体积敏感的场景,但在处理复杂目录结构时不如 chokidar 健壮。

  • nodemon:

    如果你的目标是在开发过程中自动重启 Node.js 服务器,直接使用 nodemon。不要试图用它来做通用的文件监听任务,它的配置和事件系统都是为进程重启设计的,不适合构建流水线或数据同步场景。

  • watch:

    避免在新项目中使用 watch 包。它是非常早期的实现,缺乏对现代 Node.js 特性的支持,且长期未更新。其功能完全可以通过 chokidar 或原生 API 更好地实现。

chokidar的README

Chokidar Weekly downloads

Minimal and efficient cross-platform file watching library

Why?

There are many reasons to prefer Chokidar to raw fs.watch / fs.watchFile in 2026:

  • Events are properly reported
    • macOS events report filenames
    • events are not reported twice
    • changes are reported as add / change / unlink instead of useless rename
  • Atomic writes are supported, using atomic option
    • Some file editors use them
  • Chunked writes are supported, using awaitWriteFinish option
    • Large files are commonly written in chunks
  • File / dir filtering is supported
  • Symbolic links are supported
  • Recursive watching is always supported, instead of partial when using raw events
    • Includes a way to limit recursion depth

Chokidar relies on the Node.js core fs module, but when using fs.watch and fs.watchFile for watching, it normalizes the events it receives, often checking for truth by getting file stats and/or dir contents. The fs.watch-based implementation is the default, which avoids polling and keeps CPU usage down. Be advised that chokidar will initiate watchers recursively for everything within scope of the paths that have been specified, so be judicious about not wasting system resources by watching much more than needed. For some cases, fs.watchFile, which utilizes polling and uses more resources, is used.

Made for Brunch in 2012, it is now used in ~30 million repositories and has proven itself in production environments.

  • Nov 2025 update: v5 is out. Makes package ESM-only and increases minimum node.js requirement to v20.
  • Sep 2024 update: v4 is out! It decreases dependency count from 13 to 1, removes support for globs, adds support for ESM / Common.js modules, and bumps minimum node.js version from v8 to v14. Check out upgrading.

Getting started

Install with npm:

npm install chokidar

Use it in your code:

import chokidar from 'chokidar';

// One-liner for current directory
chokidar.watch('.').on('all', (event, path) => {
  console.log(event, path);
});

// Extended options
// ----------------

// Initialize watcher.
const watcher = chokidar.watch('file, dir, or array', {
  ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
  persistent: true,
});

// Something to use when events are received.
const log = console.log.bind(console);
// Add event listeners.
watcher
  .on('add', (path) => log(`File ${path} has been added`))
  .on('change', (path) => log(`File ${path} has been changed`))
  .on('unlink', (path) => log(`File ${path} has been removed`));

// More possible events.
watcher
  .on('addDir', (path) => log(`Directory ${path} has been added`))
  .on('unlinkDir', (path) => log(`Directory ${path} has been removed`))
  .on('error', (error) => log(`Watcher error: ${error}`))
  .on('ready', () => log('Initial scan complete. Ready for changes'))
  .on('raw', (event, path, details) => {
    // internal
    log('Raw event info:', event, path, details);
  });

// 'add', 'addDir' and 'change' events also receive stat() results as second
// argument when available: https://nodejs.org/api/fs.html#fs_class_fs_stats
watcher.on('change', (path, stats) => {
  if (stats) console.log(`File ${path} changed size to ${stats.size}`);
});

// Watch new files.
watcher.add('new-file');
watcher.add(['new-file-2', 'new-file-3']);

// Get list of actual paths being watched on the filesystem
let watchedPaths = watcher.getWatched();

// Un-watch some files.
await watcher.unwatch('new-file');

// Stop watching. The method is async!
await watcher.close().then(() => console.log('closed'));

// Full list of options. See below for descriptions.
// Do not use this example!
chokidar.watch('file', {
  persistent: true,

  // ignore .txt files
  ignored: (file) => file.endsWith('.txt'),
  // watch only .txt files
  // ignored: (file, _stats) => _stats?.isFile() && !file.endsWith('.txt'),

  awaitWriteFinish: true, // emit single event when chunked writes are completed
  atomic: true, // emit proper events when "atomic writes" (mv _tmp file) are used

  // The options also allow specifying custom intervals in ms
  // awaitWriteFinish: {
  //   stabilityThreshold: 2000,
  //   pollInterval: 100
  // },
  // atomic: 100,

  interval: 100,
  binaryInterval: 300,

  cwd: '.',
  depth: 99,

  followSymlinks: true,
  ignoreInitial: false,
  ignorePermissionErrors: false,
  usePolling: false,
  alwaysStat: false,
});

chokidar.watch(paths, [options])

  • paths (string or array of strings). Paths to files, dirs to be watched recursively.
  • options (object) Options object as defined below:

Persistence

  • persistent (default: true). Indicates whether the process should continue to run as long as files are being watched.

Path filtering

  • ignored function, regex, or path. Defines files/paths to be ignored. The whole relative or absolute path is tested, not just filename. If a function with two arguments is provided, it gets called twice per path - once with a single argument (the path), second time with two arguments (the path and the fs.Stats object of that path).
  • ignoreInitial (default: false). If set to false then add/addDir events are also emitted for matching paths while instantiating the watching as chokidar discovers these file paths (before the ready event).
  • followSymlinks (default: true). When false, only the symlinks themselves will be watched for changes instead of following the link references and bubbling events through the link's path.
  • cwd (no default). The base directory from which watch paths are to be derived. Paths emitted with events will be relative to this.

Performance

  • usePolling (default: false). Whether to use fs.watchFile (backed by polling), or fs.watch. If polling leads to high CPU utilization, consider setting this to false. It is typically necessary to set this to true to successfully watch files over a network, and it may be necessary to successfully watch files in other non-standard situations. Setting to true explicitly on MacOS overrides the useFsEvents default. You may also set the CHOKIDAR_USEPOLLING env variable to true (1) or false (0) in order to override this option.
  • Polling-specific settings (effective when usePolling: true)
    • interval (default: 100). Interval of file system polling, in milliseconds. You may also set the CHOKIDAR_INTERVAL env variable to override this option.
    • binaryInterval (default: 300). Interval of file system polling for binary files. (see list of binary extensions)
  • alwaysStat (default: false). If relying upon the fs.Stats object that may get passed with add, addDir, and change events, set this to true to ensure it is provided even in cases where it wasn't already available from the underlying watch events.
  • depth (default: undefined). If set, limits how many levels of subdirectories will be traversed.
  • awaitWriteFinish (default: false). By default, the add event will fire when a file first appears on disk, before the entire file has been written. Furthermore, in some cases some change events will be emitted while the file is being written. In some cases, especially when watching for large files there will be a need to wait for the write operation to finish before responding to a file creation or modification. Setting awaitWriteFinish to true (or a truthy value) will poll file size, holding its add and change events until the size does not change for a configurable amount of time. The appropriate duration setting is heavily dependent on the OS and hardware. For accurate detection this parameter should be relatively high, making file watching much less responsive. Use with caution.
    • options.awaitWriteFinish can be set to an object in order to adjust timing params:
    • awaitWriteFinish.stabilityThreshold (default: 2000). Amount of time in milliseconds for a file size to remain constant before emitting its event.
    • awaitWriteFinish.pollInterval (default: 100). File size polling interval, in milliseconds.

Errors

  • ignorePermissionErrors (default: false). Indicates whether to watch files that don't have read permissions if possible. If watching fails due to EPERM or EACCES with this set to true, the errors will be suppressed silently.
  • atomic (default: true if useFsEvents and usePolling are false). Automatically filters out artifacts that occur when using editors that use "atomic writes" instead of writing directly to the source file. If a file is re-added within 100 ms of being deleted, Chokidar emits a change event rather than unlink then add. If the default of 100 ms does not work well for you, you can override it by setting atomic to a custom value, in milliseconds.

Methods & Events

chokidar.watch() produces an instance of FSWatcher. Methods of FSWatcher:

  • .add(path / paths): Add files, directories for tracking. Takes an array of strings or just one string.
  • .on(event, callback): Listen for an FS event. Available events: add, addDir, change, unlink, unlinkDir, ready, raw, error. Additionally all is available which gets emitted with the underlying event name and path for every event other than ready, raw, and error. raw is internal, use it carefully.
  • .unwatch(path / paths): Stop watching files or directories. Takes an array of strings or just one string.
  • .close(): async Removes all listeners from watched files. Asynchronous, returns Promise. Use with await to ensure bugs don't happen.
  • .getWatched(): Returns an object representing all the paths on the file system being watched by this FSWatcher instance. The object's keys are all the directories (using absolute paths unless the cwd option was used), and the values are arrays of the names of the items contained in each directory.

CLI

Check out third party chokidar-cli, which allows to execute a command on each change, or get a stdio stream of change events.

Troubleshooting

Sometimes, Chokidar runs out of file handles, causing EMFILE and ENOSP errors:

  • bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell
  • Error: watch /home/ ENOSPC

There are two things that can cause it.

  1. Exhausted file handles for generic fs operations
    • Can be solved by using graceful-fs, which can monkey-patch native fs module used by chokidar: let fs = require('fs'); let grfs = require('graceful-fs'); grfs.gracefulify(fs);
    • Can also be solved by tuning OS: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p.
  2. Exhausted file handles for fs.watch
    • Can't seem to be solved by graceful-fs or OS tuning
    • It's possible to start using usePolling: true, which will switch backend to resource-intensive fs.watchFile

All fsevents-related issues (WARN optional dep failed, fsevents is not a constructor) are solved by upgrading to v4+.

Changelog

  • v4 (Sep 2024): remove glob support and bundled fsevents. Decrease dependency count from 13 to 1. Rewrite in typescript. Bumps minimum node.js requirement to v14+
  • v3 (Apr 2019): massive CPU & RAM consumption improvements; reduces deps / package size by a factor of 17x and bumps Node.js requirement to v8.16+.
  • v2 (Dec 2017): globs are now posix-style-only. Tons of bugfixes.
  • v1 (Apr 2015): glob support, symlink support, tons of bugfixes. Node 0.8+ is supported
  • v0.1 (Apr 2012): Initial release, extracted from Brunch

Upgrading

If you've used globs before and want do replicate the functionality with v4:

// v3
chok.watch('**/*.js');
chok.watch('./directory/**/*');

// v4
chok.watch('.', {
  ignored: (path, stats) => stats?.isFile() && !path.endsWith('.js'), // only watch js files
});
chok.watch('./directory');

// other way
import { glob } from 'node:fs/promises';
const watcher = watch(await Array.fromAsync(glob('**/*.js')));

// unwatching
// v3
chok.unwatch('**/*.js');
// v4
chok.unwatch(await Array.fromAsync(glob('**/*.js')));

Also

Why was chokidar named this way? What's the meaning behind it?

Chowkidar is a transliteration of a Hindi word meaning 'watchman, gatekeeper', चौकीदार. This ultimately comes from Sanskrit _ चतुष्क_ (crossway, quadrangle, consisting-of-four). This word is also used in other languages like Urdu as (چوکیدار) which is widely used in Pakistan and India.

License

MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file.