Skip to content

Commit 4200a1e

Browse files
Fix slow incremental builds (especially on Windows) (#17511)
This PR fixes slow rebuilds on Windows where rebuilds go from ~2s to ~20ms. Fixes: #16911 Fixes: #17522 ## Test plan 1. Tested it on a reproduction with the following results: Before: https://github.com/user-attachments/assets/10c5e9e0-3c41-4e1d-95f6-ee8d856577ef After: https://github.com/user-attachments/assets/2c7597e9-3fff-4922-a2da-a8d06eab9047 Zooming in on the times, it looks like this: <img width="674" alt="image" src="https://github.com/user-attachments/assets/85eee69c-bbf6-4c28-8ce3-6dcdad74be9c" /> But with these changes: <img width="719" alt="image" src="https://github.com/user-attachments/assets/d89cefda-0711-4f84-bfaf-2bea11977bf7" /> We also tested this on Windows with the following results: Before: <img width="961" alt="image" src="https://github.com/user-attachments/assets/3a42f822-f103-4598-9a91-e659ae09800c" /> After: <img width="956" alt="image" src="https://github.com/user-attachments/assets/05b6b6bc-d107-40d1-a207-3638aba3fc3a" /> [ci-all] --------- Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent 60b0da9 commit 4200a1e

File tree

6 files changed

+170
-9
lines changed

6 files changed

+170
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- PostCSS: Fix race condition when two changes are queued concurrently ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
1717
- PostCSS: Ensure we process files containing an `@tailwind utilities;` directive ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
1818
- Ensure the `color-mix(…)` polyfill creates fallbacks even when using colors that can not be statically analyzed ([#17513](https://github.com/tailwindlabs/tailwindcss/pull/17513))
19+
- Fix slow incremental builds with `@tailwindcss/vite` and `@tailwindcss/postscss` (especially on Windows) ([#17511](https://github.com/tailwindlabs/tailwindcss/pull/17511))
1920

2021
## [4.1.1] - 2025-04-02
2122

crates/oxide/src/scanner/detect_sources.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::scanner::auto_source_detection::IGNORED_CONTENT_DIRS;
12
use crate::GlobEntry;
23
use fxhash::FxHashSet;
34
use globwalk::DirEntry;
@@ -101,6 +102,17 @@ pub fn resolve_globs(
101102
continue;
102103
}
103104

105+
if IGNORED_CONTENT_DIRS
106+
.iter()
107+
.any(|dir| match path.file_name() {
108+
Some(name) => name == *dir,
109+
None => false,
110+
})
111+
{
112+
it.skip_current_dir();
113+
continue;
114+
}
115+
104116
if !allowed_paths.contains(path) {
105117
continue;
106118
}

crates/oxide/src/scanner/mod.rs

+18-7
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ pub struct Scanner {
9393
dirs: Vec<PathBuf>,
9494

9595
/// All generated globs, used for setting up watchers
96-
globs: Vec<GlobEntry>,
96+
globs: Option<Vec<GlobEntry>>,
9797

9898
/// Track unique set of candidates
9999
candidates: FxHashSet<String>,
@@ -296,16 +296,24 @@ impl Scanner {
296296

297297
#[tracing::instrument(skip_all)]
298298
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
299+
if let Some(globs) = &self.globs {
300+
return globs.clone();
301+
}
302+
299303
self.scan_sources();
300304

305+
let mut globs = vec![];
301306
for source in self.sources.iter() {
302307
match source {
303308
SourceEntry::Auto { base } | SourceEntry::External { base } => {
304-
let globs = resolve_globs((base).to_path_buf(), &self.dirs, &self.extensions);
305-
self.globs.extend(globs);
309+
globs.extend(resolve_globs(
310+
base.to_path_buf(),
311+
&self.dirs,
312+
&self.extensions,
313+
));
306314
}
307315
SourceEntry::Pattern { base, pattern } => {
308-
self.globs.push(GlobEntry {
316+
globs.push(GlobEntry {
309317
base: base.to_string_lossy().to_string(),
310318
pattern: pattern.to_string(),
311319
});
@@ -315,13 +323,16 @@ impl Scanner {
315323
}
316324

317325
// Re-optimize the globs to reduce the number of patterns we have to scan.
318-
self.globs = optimize_patterns(&self.globs);
326+
globs = optimize_patterns(&globs);
327+
328+
// Track the globs for subsequent calls
329+
self.globs = Some(globs.clone());
319330

320-
self.globs.clone()
331+
globs
321332
}
322333

323334
#[tracing::instrument(skip_all)]
324-
pub fn get_normalized_sources(&mut self) -> Vec<GlobEntry> {
335+
pub fn get_normalized_sources(&self) -> Vec<GlobEntry> {
325336
self.sources
326337
.iter()
327338
.filter_map(|source| match source {

integrations/postcss/next.test.ts

+129-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe } from 'vitest'
2-
import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils'
2+
import { candidate, css, fetchStyles, js, json, jsx, retryAssertion, test, txt } from '../utils'
33

44
test(
55
'production build',
@@ -356,3 +356,131 @@ test(
356356
})
357357
},
358358
)
359+
360+
test(
361+
'changes to `public/` should not trigger an infinite loop',
362+
{
363+
fs: {
364+
'package.json': json`
365+
{
366+
"dependencies": {
367+
"react": "^18",
368+
"react-dom": "^18",
369+
"next": "^15",
370+
"@ducanh2912/next-pwa": "^10.2.9"
371+
},
372+
"devDependencies": {
373+
"@tailwindcss/postcss": "workspace:^",
374+
"tailwindcss": "workspace:^"
375+
}
376+
}
377+
`,
378+
'.gitignore': txt`
379+
.next/
380+
public/workbox-*.js
381+
public/sw.js
382+
`,
383+
'postcss.config.mjs': js`
384+
export default {
385+
plugins: {
386+
'@tailwindcss/postcss': {},
387+
},
388+
}
389+
`,
390+
'next.config.mjs': js`
391+
import withPWA from '@ducanh2912/next-pwa'
392+
393+
const pwaConfig = {
394+
dest: 'public',
395+
register: true,
396+
skipWaiting: true,
397+
reloadOnOnline: false,
398+
cleanupOutdatedCaches: true,
399+
clientsClaim: true,
400+
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
401+
}
402+
403+
const nextConfig = {}
404+
405+
const configWithPWA = withPWA(pwaConfig)(nextConfig)
406+
407+
export default configWithPWA
408+
`,
409+
'app/layout.js': js`
410+
import './globals.css'
411+
412+
export default function RootLayout({ children }) {
413+
return (
414+
<html>
415+
<body>{children}</body>
416+
</html>
417+
)
418+
}
419+
`,
420+
'app/page.js': js`
421+
export default function Page() {
422+
return <div className="flex"></div>
423+
}
424+
`,
425+
'app/globals.css': css`
426+
@import 'tailwindcss/theme';
427+
@import 'tailwindcss/utilities';
428+
`,
429+
},
430+
},
431+
async ({ spawn, fs, expect }) => {
432+
let process = await spawn('pnpm next dev')
433+
434+
let url = ''
435+
await process.onStdout((m) => {
436+
let match = /Local:\s*(http.*)/.exec(m)
437+
if (match) url = match[1]
438+
return Boolean(url)
439+
})
440+
441+
await process.onStdout((m) => m.includes('Ready in'))
442+
443+
await retryAssertion(async () => {
444+
let css = await fetchStyles(url)
445+
expect(css).toContain(candidate`flex`)
446+
expect(css).not.toContain(candidate`underline`)
447+
})
448+
449+
await fs.write(
450+
'app/page.js',
451+
jsx`
452+
export default function Page() {
453+
return <div className="flex underline"></div>
454+
}
455+
`,
456+
)
457+
await process.onStdout((m) => m.includes('Compiled in'))
458+
459+
await retryAssertion(async () => {
460+
let css = await fetchStyles(url)
461+
expect(css).toContain(candidate`flex`)
462+
expect(css).toContain(candidate`underline`)
463+
})
464+
// Flush all existing messages in the queue
465+
process.flush()
466+
467+
// Fetch the styles one more time, to ensure we see the latest version of
468+
// the CSS
469+
await fetchStyles(url)
470+
471+
// At this point, no changes should triger a compile step. If we see any
472+
// changes, there is an infinite loop because we (the user) didn't write any
473+
// files to disk.
474+
//
475+
// Ensure there are no more changes in stdout (signaling no infinite loop)
476+
let result = await Promise.race([
477+
// If this succeeds, it means that it saw another change which indicates
478+
// an infinite loop.
479+
process.onStdout((m) => m.includes('Compiled in')).then(() => 'infinite loop detected'),
480+
481+
// There should be no changes in stdout
482+
new Promise((resolve) => setTimeout(() => resolve('no infinite loop detected'), 2_000)),
483+
])
484+
expect(result).toBe('no infinite loop detected')
485+
},
486+
)

integrations/utils.ts

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((na
1515

1616
interface SpawnedProcess {
1717
dispose: () => void
18+
flush: () => void
1819
onStdout: (predicate: (message: string) => boolean) => Promise<void>
1920
onStderr: (predicate: (message: string) => boolean) => Promise<void>
2021
}
@@ -253,6 +254,13 @@ export function test(
253254

254255
return {
255256
dispose,
257+
flush() {
258+
stdoutActors.splice(0)
259+
stderrActors.splice(0)
260+
261+
stdoutMessages.splice(0)
262+
stderrMessages.splice(0)
263+
},
256264
onStdout(predicate: (message: string) => boolean) {
257265
return new Promise<void>((resolve) => {
258266
stdoutActors.push({ predicate, resolve })
@@ -504,6 +512,7 @@ export let css = dedent
504512
export let html = dedent
505513
export let ts = dedent
506514
export let js = dedent
515+
export let jsx = dedent
507516
export let json = dedent
508517
export let yaml = dedent
509518
export let txt = dedent

integrations/webpack/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { css, html, js, json, test } from '../utils'
22

33
test(
4-
'Webpack + PostCSS (watch)',
4+
'webpack + PostCSS (watch)',
55
{
66
fs: {
77
'package.json': json`

0 commit comments

Comments
 (0)