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>
This commit is contained in:
parent
60b0da90ce
commit
4200a1ecc4
6 changed files with 170 additions and 9 deletions
|
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- PostCSS: Fix race condition when two changes are queued concurrently ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
|
||||
- PostCSS: Ensure we process files containing an `@tailwind utilities;` directive ([#17514](https://github.com/tailwindlabs/tailwindcss/pull/17514))
|
||||
- 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))
|
||||
- Fix slow incremental builds with `@tailwindcss/vite` and `@tailwindcss/postscss` (especially on Windows) ([#17511](https://github.com/tailwindlabs/tailwindcss/pull/17511))
|
||||
|
||||
## [4.1.1] - 2025-04-02
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::scanner::auto_source_detection::IGNORED_CONTENT_DIRS;
|
||||
use crate::GlobEntry;
|
||||
use fxhash::FxHashSet;
|
||||
use globwalk::DirEntry;
|
||||
|
|
@ -101,6 +102,17 @@ pub fn resolve_globs(
|
|||
continue;
|
||||
}
|
||||
|
||||
if IGNORED_CONTENT_DIRS
|
||||
.iter()
|
||||
.any(|dir| match path.file_name() {
|
||||
Some(name) => name == *dir,
|
||||
None => false,
|
||||
})
|
||||
{
|
||||
it.skip_current_dir();
|
||||
continue;
|
||||
}
|
||||
|
||||
if !allowed_paths.contains(path) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ pub struct Scanner {
|
|||
dirs: Vec<PathBuf>,
|
||||
|
||||
/// All generated globs, used for setting up watchers
|
||||
globs: Vec<GlobEntry>,
|
||||
globs: Option<Vec<GlobEntry>>,
|
||||
|
||||
/// Track unique set of candidates
|
||||
candidates: FxHashSet<String>,
|
||||
|
|
@ -296,16 +296,24 @@ impl Scanner {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_globs(&mut self) -> Vec<GlobEntry> {
|
||||
if let Some(globs) = &self.globs {
|
||||
return globs.clone();
|
||||
}
|
||||
|
||||
self.scan_sources();
|
||||
|
||||
let mut globs = vec![];
|
||||
for source in self.sources.iter() {
|
||||
match source {
|
||||
SourceEntry::Auto { base } | SourceEntry::External { base } => {
|
||||
let globs = resolve_globs((base).to_path_buf(), &self.dirs, &self.extensions);
|
||||
self.globs.extend(globs);
|
||||
globs.extend(resolve_globs(
|
||||
base.to_path_buf(),
|
||||
&self.dirs,
|
||||
&self.extensions,
|
||||
));
|
||||
}
|
||||
SourceEntry::Pattern { base, pattern } => {
|
||||
self.globs.push(GlobEntry {
|
||||
globs.push(GlobEntry {
|
||||
base: base.to_string_lossy().to_string(),
|
||||
pattern: pattern.to_string(),
|
||||
});
|
||||
|
|
@ -315,13 +323,16 @@ impl Scanner {
|
|||
}
|
||||
|
||||
// Re-optimize the globs to reduce the number of patterns we have to scan.
|
||||
self.globs = optimize_patterns(&self.globs);
|
||||
globs = optimize_patterns(&globs);
|
||||
|
||||
self.globs.clone()
|
||||
// Track the globs for subsequent calls
|
||||
self.globs = Some(globs.clone());
|
||||
|
||||
globs
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn get_normalized_sources(&mut self) -> Vec<GlobEntry> {
|
||||
pub fn get_normalized_sources(&self) -> Vec<GlobEntry> {
|
||||
self.sources
|
||||
.iter()
|
||||
.filter_map(|source| match source {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe } from 'vitest'
|
||||
import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils'
|
||||
import { candidate, css, fetchStyles, js, json, jsx, retryAssertion, test, txt } from '../utils'
|
||||
|
||||
test(
|
||||
'production build',
|
||||
|
|
@ -356,3 +356,131 @@ test(
|
|||
})
|
||||
},
|
||||
)
|
||||
|
||||
test(
|
||||
'changes to `public/` should not trigger an infinite loop',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "^15",
|
||||
"@ducanh2912/next-pwa": "^10.2.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "workspace:^",
|
||||
"tailwindcss": "workspace:^"
|
||||
}
|
||||
}
|
||||
`,
|
||||
'.gitignore': txt`
|
||||
.next/
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
`,
|
||||
'postcss.config.mjs': js`
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
`,
|
||||
'next.config.mjs': js`
|
||||
import withPWA from '@ducanh2912/next-pwa'
|
||||
|
||||
const pwaConfig = {
|
||||
dest: 'public',
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
reloadOnOnline: false,
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
|
||||
}
|
||||
|
||||
const nextConfig = {}
|
||||
|
||||
const configWithPWA = withPWA(pwaConfig)(nextConfig)
|
||||
|
||||
export default configWithPWA
|
||||
`,
|
||||
'app/layout.js': js`
|
||||
import './globals.css'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
`,
|
||||
'app/page.js': js`
|
||||
export default function Page() {
|
||||
return <div className="flex"></div>
|
||||
}
|
||||
`,
|
||||
'app/globals.css': css`
|
||||
@import 'tailwindcss/theme';
|
||||
@import 'tailwindcss/utilities';
|
||||
`,
|
||||
},
|
||||
},
|
||||
async ({ spawn, fs, expect }) => {
|
||||
let process = await spawn('pnpm next dev')
|
||||
|
||||
let url = ''
|
||||
await process.onStdout((m) => {
|
||||
let match = /Local:\s*(http.*)/.exec(m)
|
||||
if (match) url = match[1]
|
||||
return Boolean(url)
|
||||
})
|
||||
|
||||
await process.onStdout((m) => m.includes('Ready in'))
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(url)
|
||||
expect(css).toContain(candidate`flex`)
|
||||
expect(css).not.toContain(candidate`underline`)
|
||||
})
|
||||
|
||||
await fs.write(
|
||||
'app/page.js',
|
||||
jsx`
|
||||
export default function Page() {
|
||||
return <div className="flex underline"></div>
|
||||
}
|
||||
`,
|
||||
)
|
||||
await process.onStdout((m) => m.includes('Compiled in'))
|
||||
|
||||
await retryAssertion(async () => {
|
||||
let css = await fetchStyles(url)
|
||||
expect(css).toContain(candidate`flex`)
|
||||
expect(css).toContain(candidate`underline`)
|
||||
})
|
||||
// Flush all existing messages in the queue
|
||||
process.flush()
|
||||
|
||||
// Fetch the styles one more time, to ensure we see the latest version of
|
||||
// the CSS
|
||||
await fetchStyles(url)
|
||||
|
||||
// At this point, no changes should triger a compile step. If we see any
|
||||
// changes, there is an infinite loop because we (the user) didn't write any
|
||||
// files to disk.
|
||||
//
|
||||
// Ensure there are no more changes in stdout (signaling no infinite loop)
|
||||
let result = await Promise.race([
|
||||
// If this succeeds, it means that it saw another change which indicates
|
||||
// an infinite loop.
|
||||
process.onStdout((m) => m.includes('Compiled in')).then(() => 'infinite loop detected'),
|
||||
|
||||
// There should be no changes in stdout
|
||||
new Promise((resolve) => setTimeout(() => resolve('no infinite loop detected'), 2_000)),
|
||||
])
|
||||
expect(result).toBe('no infinite loop detected')
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((na
|
|||
|
||||
interface SpawnedProcess {
|
||||
dispose: () => void
|
||||
flush: () => void
|
||||
onStdout: (predicate: (message: string) => boolean) => Promise<void>
|
||||
onStderr: (predicate: (message: string) => boolean) => Promise<void>
|
||||
}
|
||||
|
|
@ -253,6 +254,13 @@ export function test(
|
|||
|
||||
return {
|
||||
dispose,
|
||||
flush() {
|
||||
stdoutActors.splice(0)
|
||||
stderrActors.splice(0)
|
||||
|
||||
stdoutMessages.splice(0)
|
||||
stderrMessages.splice(0)
|
||||
},
|
||||
onStdout(predicate: (message: string) => boolean) {
|
||||
return new Promise<void>((resolve) => {
|
||||
stdoutActors.push({ predicate, resolve })
|
||||
|
|
@ -504,6 +512,7 @@ export let css = dedent
|
|||
export let html = dedent
|
||||
export let ts = dedent
|
||||
export let js = dedent
|
||||
export let jsx = dedent
|
||||
export let json = dedent
|
||||
export let yaml = dedent
|
||||
export let txt = dedent
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { css, html, js, json, test } from '../utils'
|
||||
|
||||
test(
|
||||
'Webpack + PostCSS (watch)',
|
||||
'webpack + PostCSS (watch)',
|
||||
{
|
||||
fs: {
|
||||
'package.json': json`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue