Skip to content

Commit bd5833d

Browse files
committed
fix: speed up v3 vite watch runtime updates
1 parent e1ef3d1 commit bd5833d

7 files changed

Lines changed: 245 additions & 20 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"weapp-tailwindcss": patch
3+
---
4+
5+
修复 Tailwind CSS v3 在 Vite watch 热更新中因源码类名变化反复触发完整 runtime extract 导致 HMR 变慢的问题。v3 首轮仍保留完整 runtime 基线,后续 watch 轮次按文件增量更新源码候选类,避免已删除源码类继续污染 CSS,同时保留 safelist 等非源码基线类。

packages/weapp-tailwindcss/src/bundlers/vite/generate-bundle.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,10 @@ export function createGenerateBundleHook(context: GenerateBundleContext) {
139139
const filteredGeneratorCandidates = shouldFilterTailwindV4MiniProgramCandidates
140140
? filterUnsupportedMiniProgramTailwindV4Candidates(collectedGeneratorCandidates)
141141
: collectedGeneratorCandidates
142-
const generatorRuntime = collectLegacyContainerCompatCandidates(
142+
let generatorRuntime = collectLegacyContainerCompatCandidates(
143143
sourceCandidates,
144144
filteredGeneratorCandidates,
145145
)
146-
const generatorCandidateSignature = createCandidateSignature(generatorRuntime)
147-
recordGeneratorCandidates?.(generatorRuntime)
148146
let transformRuntime = runtime
149147
if (runtimeState.twPatcher.majorVersion === 3 && generatorRuntime.size > 0) {
150148
const cssEntries = snapshot.entries.filter(entry =>
@@ -163,10 +161,20 @@ export function createGenerateBundleHook(context: GenerateBundleContext) {
163161
debug,
164162
})
165163
if (validatedRuntime.size > 0) {
166-
transformRuntime = new Set([...runtime, ...validatedRuntime])
164+
generatorRuntime = collectLegacyContainerCompatCandidates(
165+
sourceCandidates,
166+
validatedRuntime,
167+
)
168+
transformRuntime = generatorRuntime
169+
}
170+
else {
171+
generatorRuntime = validatedRuntime
172+
transformRuntime = validatedRuntime
167173
}
168174
}
169175
}
176+
const generatorCandidateSignature = createCandidateSignature(generatorRuntime)
177+
recordGeneratorCandidates?.(generatorRuntime)
170178
const defaultTemplateHandlerOptions = {
171179
runtimeSet: transformRuntime,
172180
}

packages/weapp-tailwindcss/src/bundlers/vite/incremental-runtime-class-set.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ type ExtractRawCandidateResult = Awaited<ReturnType<typeof extractRawCandidatesW
1818
type ExtractRawCandidatesFn = (content: string, extension?: string) => Promise<ExtractRawCandidateResult>
1919

2020
export interface BundleRuntimeClassSetManager {
21-
sync: (patcher: TailwindcssPatcherLike, snapshot: BundleSnapshot) => Promise<Set<string>>
21+
sync: (patcher: TailwindcssPatcherLike, snapshot: BundleSnapshot, options?: BundleRuntimeClassSetSyncOptions) => Promise<Set<string>>
2222
reset: () => Promise<void>
2323
}
2424

25+
export interface BundleRuntimeClassSetSyncOptions {
26+
baseClassSet?: Set<string> | undefined
27+
}
28+
2529
interface CreateBundleRuntimeClassSetManagerOptions {
2630
extractCandidates?: ExtractValidCandidatesFn
2731
extractRawCandidates?: ExtractRawCandidatesFn
@@ -71,7 +75,6 @@ function createCandidateValidationSource(candidates: Iterable<string>) {
7175

7276
function removeCandidateSet(
7377
candidateCountByClass: Map<string, number>,
74-
runtimeSet: Set<string>,
7578
candidates: Set<string>,
7679
) {
7780
for (const className of candidates) {
@@ -81,7 +84,6 @@ function removeCandidateSet(
8184
}
8285
if (count <= 1) {
8386
candidateCountByClass.delete(className)
84-
runtimeSet.delete(className)
8587
continue
8688
}
8789
candidateCountByClass.set(className, count - 1)
@@ -90,23 +92,42 @@ function removeCandidateSet(
9092

9193
function addCandidateSet(
9294
candidateCountByClass: Map<string, number>,
93-
runtimeSet: Set<string>,
9495
candidates: Set<string>,
9596
) {
9697
for (const className of candidates) {
9798
const nextCount = (candidateCountByClass.get(className) ?? 0) + 1
9899
candidateCountByClass.set(className, nextCount)
99-
runtimeSet.add(className)
100100
}
101101
}
102102

103+
function createRuntimeClassSet(
104+
baseClassSet: Set<string>,
105+
candidateCountByClass: Map<string, number>,
106+
) {
107+
return new Set([
108+
...baseClassSet,
109+
...candidateCountByClass.keys(),
110+
])
111+
}
112+
113+
function createNonSourceBaseClassSet(
114+
baseClassSet: Set<string>,
115+
candidateCountByClass: Map<string, number>,
116+
) {
117+
const nextBaseClassSet = new Set(baseClassSet)
118+
for (const candidate of candidateCountByClass.keys()) {
119+
nextBaseClassSet.delete(candidate)
120+
}
121+
return nextBaseClassSet
122+
}
123+
103124
export function createBundleRuntimeClassSetManager(
104125
options: CreateBundleRuntimeClassSetManagerOptions = {},
105126
): BundleRuntimeClassSetManager {
106127
const customExtractCandidates = options.extractCandidates
107128
const extractCandidates = customExtractCandidates ?? extractValidCandidates
108129
const extractRawCandidates = options.extractRawCandidates ?? extractRawCandidatesWithPositions
109-
const runtimeSet = new Set<string>()
130+
let baseClassSet = new Set<string>()
110131
const candidateCountByClass = new Map<string, number>()
111132
const candidatesByFile = new Map<string, Set<string>>()
112133
const candidateValidityCache = new Map<string, boolean>()
@@ -115,7 +136,7 @@ export function createBundleRuntimeClassSetManager(
115136
let designSystemPromise: Promise<TailwindV4DesignSystem> | undefined
116137

117138
async function reset() {
118-
runtimeSet.clear()
139+
baseClassSet = new Set<string>()
119140
candidateCountByClass.clear()
120141
candidatesByFile.clear()
121142
candidateValidityCache.clear()
@@ -169,6 +190,13 @@ export function createBundleRuntimeClassSetManager(
169190
return
170191
}
171192

193+
if (patcher.majorVersion === 3 && !customExtractCandidates) {
194+
for (const candidate of unknownCandidates) {
195+
candidateValidityCache.set(candidate, true)
196+
}
197+
return
198+
}
199+
172200
const context = await resolveValidationContextCached(patcher)
173201
if (!customExtractCandidates) {
174202
try {
@@ -206,6 +234,7 @@ export function createBundleRuntimeClassSetManager(
206234
async function sync(
207235
patcher: TailwindcssPatcherLike,
208236
snapshot: BundleSnapshot,
237+
options: BundleRuntimeClassSetSyncOptions = {},
209238
) {
210239
const nextSignature = getRuntimeClassSetSignature(patcher) ?? 'runtime:missing'
211240
const runtimeEntries = createRuntimeEntries(snapshot)
@@ -219,12 +248,13 @@ export function createBundleRuntimeClassSetManager(
219248
}
220249

221250
runtimeSignature = nextSignature
251+
const nextBaseClassSet = options.baseClassSet
222252

223253
for (const [file, previousCandidates] of candidatesByFile) {
224254
if (currentRuntimeFiles.has(file)) {
225255
continue
226256
}
227-
removeCandidateSet(candidateCountByClass, runtimeSet, previousCandidates)
257+
removeCandidateSet(candidateCountByClass, previousCandidates)
228258
candidatesByFile.delete(file)
229259
}
230260

@@ -233,7 +263,10 @@ export function createBundleRuntimeClassSetManager(
233263
: [...collectChangedRuntimeFiles(snapshot)]
234264

235265
if (changedRuntimeFiles.length === 0) {
236-
return new Set(runtimeSet)
266+
if (nextBaseClassSet) {
267+
baseClassSet = createNonSourceBaseClassSet(nextBaseClassSet, candidateCountByClass)
268+
}
269+
return createRuntimeClassSet(baseClassSet, candidateCountByClass)
237270
}
238271

239272
const rawCandidatesByFile = new Map<string, Set<string>>()
@@ -261,7 +294,7 @@ export function createBundleRuntimeClassSetManager(
261294
const nextRawCandidates = rawCandidatesByFile.get(file)
262295
const previousCandidates = candidatesByFile.get(file)
263296
if (previousCandidates) {
264-
removeCandidateSet(candidateCountByClass, runtimeSet, previousCandidates)
297+
removeCandidateSet(candidateCountByClass, previousCandidates)
265298
}
266299

267300
if (!nextRawCandidates || nextRawCandidates.size === 0) {
@@ -279,10 +312,15 @@ export function createBundleRuntimeClassSetManager(
279312
continue
280313
}
281314

282-
addCandidateSet(candidateCountByClass, runtimeSet, nextCandidates)
315+
addCandidateSet(candidateCountByClass, nextCandidates)
283316
candidatesByFile.set(file, nextCandidates)
284317
}
285318

319+
if (nextBaseClassSet) {
320+
baseClassSet = createNonSourceBaseClassSet(nextBaseClassSet, candidateCountByClass)
321+
}
322+
const runtimeSet = createRuntimeClassSet(baseClassSet, candidateCountByClass)
323+
286324
debug(
287325
'incremental runtime set synced, changedFiles=%d rawCandidates=%d validateMisses=%d runtimeSize=%d trackedFiles=%d',
288326
changedRuntimeFiles.length,

packages/weapp-tailwindcss/src/bundlers/vite/runtime-class-set.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ export function createViteRuntimeClassSet(options: CreateViteRuntimeClassSetOpti
136136
}
137137
}
138138

139+
if (runtimeState.twPatcher.majorVersion === 3 && !forceRuntimeRefresh) {
140+
try {
141+
let baseClassSet: Set<string> | undefined
142+
if (!runtimeSet || shouldRefreshPatcher) {
143+
baseClassSet = await collectRuntimeClassSet(runtimeState.twPatcher, {
144+
force: true,
145+
skipRefresh: shouldRefreshPatcher,
146+
clearCache: shouldRefreshPatcher,
147+
})
148+
}
149+
const nextRuntimeSet = await bundleRuntimeClassSetManager.sync(runtimeState.twPatcher, snapshot, {
150+
baseClassSet,
151+
})
152+
runtimeSet = nextRuntimeSet
153+
return nextRuntimeSet
154+
}
155+
catch (error) {
156+
debug('incremental runtime set sync failed, fallback to full collect: %O', error)
157+
await bundleRuntimeClassSetManager.reset()
158+
}
159+
}
160+
139161
if (!forceRuntimeRefresh && !invalidation.changed && !forceCollectBySource && runtimeSet) {
140162
return runtimeSet
141163
}

packages/weapp-tailwindcss/test/bundlers/vite-incremental-runtime-class-set.unit.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ function createPatcher(projectRoot: string) {
3535
} as any
3636
}
3737

38+
function createV3Patcher() {
39+
return {
40+
majorVersion: 3,
41+
patch: vi.fn(async () => ({})),
42+
getClassSet: vi.fn(async () => new Set<string>()),
43+
extract: vi.fn(async () => ({ classSet: new Set<string>() })),
44+
options: {},
45+
} as any
46+
}
47+
3848
describe('bundlers/vite incremental runtime class set', () => {
3949
let tempRoot = ''
4050

@@ -433,4 +443,51 @@ describe('bundlers/vite incremental runtime class set', () => {
433443
await manager.sync(patcher, secondSnapshot)
434444
expect(extractCandidates).toHaveBeenCalledTimes(2)
435445
})
446+
447+
it('keeps v3 non-source baseline classes while replacing changed source candidates', async () => {
448+
const opts = createOptions()
449+
const outDir = '/project/dist'
450+
const state = createBundleBuildState()
451+
const patcher = createV3Patcher()
452+
const extractRawCandidates = vi.fn(async (content: string) => {
453+
if (content.includes('bg-blue-500')) {
454+
return [{ rawCandidate: 'bg-blue-500' }]
455+
}
456+
if (content.includes('bg-[#123455]')) {
457+
return [{ rawCandidate: 'bg-[#123455]' }]
458+
}
459+
return []
460+
})
461+
const manager = createBundleRuntimeClassSetManager({
462+
extractRawCandidates,
463+
})
464+
465+
const firstSnapshot = buildBundleSnapshot({
466+
'pages/index/index.wxml': {
467+
...createRollupAsset('<view class="bg-blue-500" />'),
468+
fileName: 'pages/index/index.wxml',
469+
},
470+
}, opts, outDir, state)
471+
472+
const firstRuntimeSet = await manager.sync(patcher, firstSnapshot, {
473+
baseClassSet: new Set(['bg-blue-500', 'safelist-only']),
474+
})
475+
476+
expect(firstRuntimeSet).toEqual(new Set(['safelist-only', 'bg-blue-500']))
477+
478+
updateBundleBuildState(state, firstSnapshot, new Map())
479+
480+
const secondSnapshot = buildBundleSnapshot({
481+
'pages/index/index.wxml': {
482+
...createRollupAsset('<view class="bg-[#123455]" />'),
483+
fileName: 'pages/index/index.wxml',
484+
},
485+
}, opts, outDir, state)
486+
487+
const secondRuntimeSet = await manager.sync(patcher, secondSnapshot)
488+
489+
expect(secondRuntimeSet).toEqual(new Set(['safelist-only', 'bg-[#123455]']))
490+
expect(secondRuntimeSet.has('bg-blue-500')).toBe(false)
491+
expect(extractRawCandidates).toHaveBeenCalledTimes(2)
492+
})
436493
})

0 commit comments

Comments
 (0)