fix(dev): stop fs.watch last-access noise from looping page reloads

On Windows hosts with NTFS last-access updates enabled (fsutil behavior
DisableLastAccess 0/2), every read of a file under src/ updates its
last-access time, and recursive fs.watch reports that as a change event.
The dev server then reloaded the page for any non-bundled path: fonts,
styles, images and other static assets fetched by the page itself.
Each reload fetched those assets again, producing a reload -> fetch ->
last-access -> reload feedback loop where any UI interaction kept
refreshing the page.

Two guards in handleFrontendChange:

- Directory-level change events arrive with a null filename and cannot
  identify a reloadable file; ignore them instead of reloading.
- A last-access update never changes mtime, so a change event whose
  mtime is far in the past is read noise, not an edit. Only reload for
  files written within the last 30 seconds. Trade-off: writers that
  deliberately preserve mtime no longer trigger a reload.

Verified on Windows (DisableLastAccess=2): two rounds of fetching 12
static assets produced zero reload events (previously 8 in one round),
while a real file write still triggered exactly one reload.
This commit is contained in:
gaogao-gao 2026-09-06 04:08:38 +01:00
parent 59bc9d6de4
commit 85cb559aef

View file

@ -218,10 +218,34 @@ function scheduleReload() {
}, 80);
}
// Events whose mtime is older than this window are not edits.
const RELOAD_MTIME_WINDOW_MS = 30_000;
function handleFrontendChange(filePath) {
if (!filePath || !isRspackOwned(filePath)) {
scheduleReload();
// Windows recursive fs.watch can report a directory-level change without a
// filename. It cannot identify a reloadable source file, so ignore it.
if (!filePath || isRspackOwned(filePath)) {
return;
}
// Windows fs.watch reports NTFS last-access updates as change events.
// When last-access updates are enabled (fsutil DisableLastAccess=0/2),
// every page load reads fonts/styles/images under src/, which fires
// events here and forms a reload -> fetch -> last-access feedback loop.
// last-access never changes mtime, so noise events carry a historical
// mtime while a real edit is always recent; discriminate on that. The
// trade-off: writers that intentionally preserve mtime do not reload.
let stat;
try {
stat = fs.statSync(filePath);
} catch {
return;
}
if (Date.now() - stat.mtimeMs > RELOAD_MTIME_WINDOW_MS) {
return;
}
scheduleReload();
}
function watchTree(root) {