Skip to content

Commit c829ef2

Browse files
committed
fs: skip watching ignored paths in recursive watcher
Instead of just filtering events, skip watching ignored paths entirely to avoid kernel resource pressure from unnecessary file watchers. This is especially important for large directories like node_modules.
1 parent fa14e42 commit c829ef2

File tree

2 files changed

+13
-15
lines changed

2 files changed

+13
-15
lines changed

lib/internal/fs/recursive_watch.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@ class FSWatcher extends EventEmitter {
9696
this.emit('close');
9797
}
9898

99-
#emitChange(eventType, filename) {
100-
// Filter events if ignore matcher is set and filename is available
101-
if (filename != null && this.#ignoreMatcher?.(filename)) {
102-
return;
103-
}
104-
this.emit('change', eventType, filename);
105-
}
106-
10799
#unwatchFiles(file) {
108100
this.#symbolicFiles.delete(file);
109101

@@ -130,9 +122,15 @@ class FSWatcher extends EventEmitter {
130122
}
131123

132124
const f = pathJoin(folder, file.name);
125+
const relativePath = pathRelative(this.#rootPath, f);
126+
127+
// Skip watching ignored paths entirely to avoid kernel resource pressure
128+
if (this.#ignoreMatcher?.(relativePath)) {
129+
continue;
130+
}
133131

134132
if (!this.#files.has(f)) {
135-
this.#emitChange('rename', pathRelative(this.#rootPath, f));
133+
this.emit('change', 'rename', relativePath);
136134

137135
if (file.isSymbolicLink()) {
138136
this.#symbolicFiles.add(f);
@@ -190,20 +188,20 @@ class FSWatcher extends EventEmitter {
190188
this.#files.delete(file);
191189
this.#watchers.delete(file);
192190
watcher.close();
193-
this.#emitChange('rename', pathRelative(this.#rootPath, file));
191+
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
194192
this.#unwatchFiles(file);
195193
} else if (file === this.#rootPath && this.#watchingFile) {
196194
// This case will only be triggered when watching a file with fs.watch
197-
this.#emitChange('change', pathBasename(file));
195+
this.emit('change', 'change', pathBasename(file));
198196
} else if (this.#symbolicFiles.has(file)) {
199197
// Stats from watchFile does not return correct value for currentStats.isSymbolicLink()
200198
// Since it is only valid when using fs.lstat(). Therefore, check the existing symbolic files.
201-
this.#emitChange('rename', pathRelative(this.#rootPath, file));
199+
this.emit('change', 'rename', pathRelative(this.#rootPath, file));
202200
} else if (currentStats.isDirectory()) {
203201
this.#watchFolder(file);
204202
} else {
205203
// Watching a directory will trigger a change event for child files)
206-
this.#emitChange('change', pathRelative(this.#rootPath, file));
204+
this.emit('change', 'change', pathRelative(this.#rootPath, file));
207205
}
208206
});
209207
this.#watchers.set(file, watcher);

test/parallel/test-fs-watch-ignore-recursive.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ tmpdir.refresh();
130130

131131
const watcher = fs.watch(testDirectory, {
132132
recursive: true,
133-
// Use array to match both the directory itself and files inside it
134-
// On macOS, FSEvents may report events for the directory when files change inside it
133+
// On Linux, matching the directory skips watching it entirely.
134+
// On macOS, the native watcher still needs to filter file events inside.
135135
ignore: ['**/node_modules/**', '**/node_modules'],
136136
});
137137

0 commit comments

Comments
 (0)