Skip to content

Commit fa30879

Browse files
authored
fix: build time deps optimization, and ensure single crawl end call (#12851)
1 parent 0212b1b commit fa30879

File tree

7 files changed

+162
-71
lines changed

7 files changed

+162
-71
lines changed

packages/vite/src/node/optimizer/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ export function runOptimizeDeps(
504504
metadata,
505505
cancel: cleanUp,
506506
commit: async () => {
507+
if (cleaned) {
508+
throw new Error(
509+
'Can not commit a Deps Optimization run as it was cancelled',
510+
)
511+
}
507512
// Ignore clean up requests after this point so the temp folder isn't deleted before
508513
// we finish commiting the new deps cache files to the deps folder
509514
committed = true

packages/vite/src/node/optimizer/optimizer.ts

Lines changed: 118 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async function createDepsOptimizer(
9999

100100
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr)
101101

102-
let handle: NodeJS.Timeout | undefined
102+
let debounceProcessingHandle: NodeJS.Timeout | undefined
103103

104104
let closed = false
105105

@@ -155,9 +155,20 @@ async function createDepsOptimizer(
155155
let enqueuedRerun: (() => void) | undefined
156156
let currentlyProcessing = false
157157

158-
// If there wasn't a cache or it is outdated, we need to prepare a first run
159158
let firstRunCalled = !!cachedMetadata
160159

160+
// During build, we wait for every module to be scanned before resolving
161+
// optimized deps loading for rollup on each rebuild. It will be recreated
162+
// after each buildStart.
163+
// During dev, if this is a cold run, we wait for static imports discovered
164+
// from the first request before resolving to minimize full page reloads.
165+
// On warm start or after the first optimization is run, we use a simpler
166+
// debounce strategy each time a new dep is discovered.
167+
let crawlEndFinder: CrawlEndFinder | undefined
168+
if (isBuild || !cachedMetadata) {
169+
crawlEndFinder = setupOnCrawlEnd(onCrawlEnd)
170+
}
171+
161172
let optimizationResult:
162173
| {
163174
cancel: () => Promise<void>
@@ -174,6 +185,7 @@ async function createDepsOptimizer(
174185

175186
async function close() {
176187
closed = true
188+
crawlEndFinder?.cancel()
177189
await Promise.allSettled([
178190
discover?.cancel(),
179191
depsOptimizer.scanProcessing,
@@ -290,7 +302,7 @@ async function createDepsOptimizer(
290302
enqueuedRerun = undefined
291303

292304
// Ensure that a rerun will not be issued for current discovered deps
293-
if (handle) clearTimeout(handle)
305+
if (debounceProcessingHandle) clearTimeout(debounceProcessingHandle)
294306

295307
if (closed || Object.keys(metadata.discovered).length === 0) {
296308
currentlyProcessing = false
@@ -529,7 +541,12 @@ async function createDepsOptimizer(
529541
// we can get a list of every missing dependency before giving to the
530542
// browser a dependency that may be outdated, thus avoiding full page reloads
531543

532-
if (firstRunCalled) {
544+
if (!crawlEndFinder) {
545+
if (isBuild) {
546+
logger.error(
547+
'Vite Internal Error: Missing dependency found after crawling ended',
548+
)
549+
}
533550
// Debounced rerun, let other missing dependencies be discovered before
534551
// the running next optimizeDeps
535552
debouncedProcessing()
@@ -570,26 +587,34 @@ async function createDepsOptimizer(
570587
// Debounced rerun, let other missing dependencies be discovered before
571588
// the running next optimizeDeps
572589
enqueuedRerun = undefined
573-
if (handle) clearTimeout(handle)
590+
if (debounceProcessingHandle) clearTimeout(debounceProcessingHandle)
574591
if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle)
575592
newDepsToLogHandle = undefined
576-
handle = setTimeout(() => {
577-
handle = undefined
593+
debounceProcessingHandle = setTimeout(() => {
594+
debounceProcessingHandle = undefined
578595
enqueuedRerun = rerun
579596
if (!currentlyProcessing) {
580597
enqueuedRerun()
581598
}
582599
}, timeout)
583600
}
584601

602+
// During dev, onCrawlEnd is called once when the server starts and all static
603+
// imports after the first request have been crawled (dynamic imports may also
604+
// be crawled if the browser requests them right away).
605+
// During build, onCrawlEnd will be called once after each buildStart (so in
606+
// watch mode it will be called after each rebuild has processed every module).
607+
// All modules are transformed first in this case (both static and dynamic).
585608
async function onCrawlEnd() {
609+
// On build time, a missing dep appearing after onCrawlEnd is an internal error
610+
// On dev, switch after this point to a simple debounce strategy
611+
crawlEndFinder = undefined
612+
586613
debug?.(colors.green(`✨ static imports crawl ended`))
587-
if (firstRunCalled) {
614+
if (closed) {
588615
return
589616
}
590617

591-
currentlyProcessing = false
592-
593618
const crawlDeps = Object.keys(metadata.discovered)
594619

595620
// Await for the scan+optimize step running in the background
@@ -599,6 +624,7 @@ async function createDepsOptimizer(
599624
if (!isBuild && optimizationResult) {
600625
const result = await optimizationResult.result
601626
optimizationResult = undefined
627+
currentlyProcessing = false
602628

603629
const scanDeps = Object.keys(result.metadata.optimized)
604630

@@ -650,6 +676,8 @@ async function createDepsOptimizer(
650676
runOptimizer(result)
651677
}
652678
} else {
679+
currentlyProcessing = false
680+
653681
if (crawlDeps.length === 0) {
654682
debug?.(
655683
colors.green(
@@ -664,30 +692,62 @@ async function createDepsOptimizer(
664692
}
665693
}
666694

667-
const runOptimizerIfIdleAfterMs = 50
695+
// Called during buildStart at build time, when build --watch is used.
696+
function resetRegisteredIds() {
697+
crawlEndFinder?.cancel()
698+
crawlEndFinder = setupOnCrawlEnd(onCrawlEnd)
699+
}
700+
701+
function registerWorkersSource(id: string) {
702+
crawlEndFinder?.registerWorkersSource(id)
703+
}
704+
function delayDepsOptimizerUntil(id: string, done: () => Promise<any>) {
705+
if (crawlEndFinder && !depsOptimizer.isOptimizedDepFile(id)) {
706+
crawlEndFinder.delayDepsOptimizerUntil(id, done)
707+
}
708+
}
709+
function ensureFirstRun() {
710+
crawlEndFinder?.ensureFirstRun()
711+
}
712+
}
668713

714+
const runOptimizerIfIdleAfterMs = 50
715+
716+
interface CrawlEndFinder {
717+
ensureFirstRun: () => void
718+
registerWorkersSource: (id: string) => void
719+
delayDepsOptimizerUntil: (id: string, done: () => Promise<any>) => void
720+
cancel: () => void
721+
}
722+
723+
function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder {
669724
let registeredIds: { id: string; done: () => Promise<any> }[] = []
670-
let seenIds = new Set<string>()
671-
let workersSources = new Set<string>()
672-
const waitingOn = new Set<string>()
725+
const seenIds = new Set<string>()
726+
const workersSources = new Set<string>()
727+
const waitingOn = new Map<string, () => void>()
673728
let firstRunEnsured = false
729+
let crawlEndCalled = false
674730

675-
function resetRegisteredIds() {
676-
registeredIds = []
677-
seenIds = new Set<string>()
678-
workersSources = new Set<string>()
679-
waitingOn.clear()
680-
firstRunEnsured = false
731+
let cancelled = false
732+
function cancel() {
733+
cancelled = true
734+
}
735+
736+
function callOnCrawlEnd() {
737+
if (!cancelled && !crawlEndCalled) {
738+
crawlEndCalled = true
739+
onCrawlEnd()
740+
}
681741
}
682742

683743
// If all the inputs are dependencies, we aren't going to get any
684744
// delayDepsOptimizerUntil(id) calls. We need to guard against this
685745
// by forcing a rerun if no deps have been registered
686746
function ensureFirstRun() {
687-
if (!firstRunEnsured && !firstRunCalled && registeredIds.length === 0) {
747+
if (!firstRunEnsured && seenIds.size === 0) {
688748
setTimeout(() => {
689-
if (!closed && registeredIds.length === 0) {
690-
onCrawlEnd()
749+
if (seenIds.size === 0) {
750+
callOnCrawlEnd()
691751
}
692752
}, runOptimizerIfIdleAfterMs)
693753
}
@@ -699,37 +759,46 @@ async function createDepsOptimizer(
699759
// Avoid waiting for this id, as it may be blocked by the rollup
700760
// bundling process of the worker that also depends on the optimizer
701761
registeredIds = registeredIds.filter((registered) => registered.id !== id)
702-
if (waitingOn.has(id)) {
703-
waitingOn.delete(id)
704-
runOptimizerWhenIdle()
705-
}
762+
763+
const resolve = waitingOn.get(id)
764+
// Forced resolve to avoid waiting for the bundling of the worker to finish
765+
resolve?.()
706766
}
707767

708768
function delayDepsOptimizerUntil(id: string, done: () => Promise<any>): void {
709-
if (!depsOptimizer.isOptimizedDepFile(id) && !seenIds.has(id)) {
769+
if (!seenIds.has(id)) {
710770
seenIds.add(id)
711771
registeredIds.push({ id, done })
712-
runOptimizerWhenIdle()
772+
callOnCrawlEndWhenIdle()
713773
}
714774
}
715775

716-
async function runOptimizerWhenIdle() {
717-
if (waitingOn.size > 0) return
776+
async function callOnCrawlEndWhenIdle() {
777+
if (cancelled || waitingOn.size > 0) return
718778

719779
const processingRegisteredIds = registeredIds
720780
registeredIds = []
721781

722782
const donePromises = processingRegisteredIds.map(async (registeredId) => {
723-
waitingOn.add(registeredId.id)
724-
try {
725-
await registeredId.done()
726-
} finally {
727-
waitingOn.delete(registeredId.id)
728-
}
783+
// During build, we need to cancel workers
784+
let resolve: () => void
785+
const waitUntilDone = new Promise<void>((_resolve) => {
786+
resolve = _resolve
787+
registeredId
788+
.done()
789+
.catch(() => {
790+
// Ignore errors
791+
})
792+
.finally(() => resolve())
793+
})
794+
waitingOn.set(registeredId.id, () => resolve())
795+
796+
await waitUntilDone
797+
waitingOn.delete(registeredId.id)
729798
})
730799

731800
const afterLoad = () => {
732-
if (closed) return
801+
if (cancelled) return
733802
if (
734803
registeredIds.length > 0 &&
735804
registeredIds.every((registeredId) =>
@@ -740,22 +809,26 @@ async function createDepsOptimizer(
740809
}
741810

742811
if (registeredIds.length > 0) {
743-
runOptimizerWhenIdle()
812+
callOnCrawlEndWhenIdle()
744813
} else {
745-
onCrawlEnd()
814+
callOnCrawlEnd()
746815
}
747816
}
748817

749-
const results = await Promise.allSettled(donePromises)
750-
if (
751-
registeredIds.length > 0 ||
752-
results.some((result) => result.status === 'rejected')
753-
) {
818+
await Promise.allSettled(donePromises)
819+
if (registeredIds.length > 0) {
754820
afterLoad()
755821
} else {
756822
setTimeout(afterLoad, runOptimizerIfIdleAfterMs)
757823
}
758824
}
825+
826+
return {
827+
ensureFirstRun,
828+
registerWorkersSource,
829+
delayDepsOptimizerUntil,
830+
cancel,
831+
}
759832
}
760833

761834
async function createDevSsrDepsOptimizer(

packages/vite/src/node/plugins/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export async function resolvePlugins(
4040
const { modulePreload } = config.build
4141

4242
return [
43+
...(isDepsOptimizerEnabled(config, false) ||
44+
isDepsOptimizerEnabled(config, true)
45+
? [
46+
isBuild
47+
? optimizedDepsBuildPlugin(config)
48+
: optimizedDepsPlugin(config),
49+
]
50+
: []),
4351
isWatch ? ensureWatchPlugin() : null,
4452
isBuild ? metadataPlugin() : null,
4553
watchPackageDataPlugin(config.packageCache),
@@ -50,14 +58,6 @@ export async function resolvePlugins(
5058
(typeof modulePreload === 'object' && modulePreload.polyfill)
5159
? modulePreloadPolyfillPlugin(config)
5260
: null,
53-
...(isDepsOptimizerEnabled(config, false) ||
54-
isDepsOptimizerEnabled(config, true)
55-
? [
56-
isBuild
57-
? optimizedDepsBuildPlugin(config)
58-
: optimizedDepsPlugin(config),
59-
]
60-
: []),
6161
resolvePlugin({
6262
...config.resolve,
6363
root: config.root,

packages/vite/src/node/plugins/optimizedDeps.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,33 +77,45 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin {
7777
}
7878

7979
export function optimizedDepsBuildPlugin(config: ResolvedConfig): Plugin {
80+
let buildStartCalled = false
81+
8082
return {
8183
name: 'vite:optimized-deps-build',
8284

8385
buildStart() {
84-
if (!config.isWorker) {
85-
// This will be run for the current active optimizer, during build
86-
// it will be the SSR optimizer if config.build.ssr is defined
86+
// Only reset the registered ids after a rebuild during build --watch
87+
if (!config.isWorker && buildStartCalled) {
8788
getDepsOptimizer(config)?.resetRegisteredIds()
8889
}
90+
buildStartCalled = true
8991
},
9092

91-
resolveId(id, importer, { ssr }) {
92-
if (getDepsOptimizer(config, ssr)?.isOptimizedDepFile(id)) {
93+
async resolveId(id, importer, options) {
94+
const depsOptimizer = getDepsOptimizer(config)
95+
if (!depsOptimizer) return
96+
97+
if (depsOptimizer.isOptimizedDepFile(id)) {
9398
return id
99+
} else {
100+
if (options?.custom?.['vite:pre-alias']) {
101+
// Skip registering the id if it is being resolved from the pre-alias plugin
102+
// When a optimized dep is aliased, we need to avoid waiting for it before optimizing
103+
return
104+
}
105+
const resolved = await this.resolve(id, importer, {
106+
...options,
107+
skipSelf: true,
108+
})
109+
if (resolved) {
110+
depsOptimizer.delayDepsOptimizerUntil(resolved.id, async () => {
111+
await this.load(resolved)
112+
})
113+
}
94114
}
95115
},
96116

97-
transform(_code, id, options) {
98-
const ssr = options?.ssr === true
99-
getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, async () => {
100-
await this.load({ id })
101-
})
102-
},
103-
104-
async load(id, options) {
105-
const ssr = options?.ssr === true
106-
const depsOptimizer = getDepsOptimizer(config, ssr)
117+
async load(id) {
118+
const depsOptimizer = getDepsOptimizer(config)
107119
if (!depsOptimizer?.isOptimizedDepFile(id)) {
108120
return
109121
}

0 commit comments

Comments
 (0)