Skip to content

Commit 9ec1f7b

Browse files
committed
perf: single chunk map
1 parent 2496007 commit 9ec1f7b

File tree

2 files changed

+70
-74
lines changed

2 files changed

+70
-74
lines changed

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

+69-60
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
ImportSpecifier,
66
} from 'es-module-lexer'
77
import { init, parse as parseImports } from 'es-module-lexer'
8-
import type { SourceMap } from 'rollup'
8+
import type { OutputChunk, SourceMap } from 'rollup'
99
import type { RawSourceMap } from '@ampproject/remapping'
1010
import convertSourceMap from 'convert-source-map'
1111
import {
@@ -22,18 +22,18 @@ import type { Environment } from '../environment'
2222
import { removedPureCssFilesCache } from './css'
2323
import { createParseErrorInfo } from './importAnalysis'
2424

25-
type FileDep = {
26-
url: string
27-
runtime: boolean
28-
}
25+
const symbolString = (name: string) =>
26+
`__viteSymbol_${name}_${Math.random().toString(36).slice(2)}__`
2927

3028
type VitePreloadErrorEvent = Event & { payload: Error }
3129

3230
// Placeholder symbols for injecting helpers
3331
export const isEsmFlag = `__VITE_IS_MODERN__`
32+
const isEsmFlagPattern = new RegExp('\\b' + isEsmFlag + '\\b', 'g')
33+
3434
export const preloadMethod = `__vitePreload`
3535
const preloadMarker = `__VITE_PRELOAD__`
36-
const viteMapDeps = '__vite__mapDeps'
36+
const chunkRegistryPlaceholder = symbolString('chunkRegistryPlaceholder')
3737

3838
export const preloadHelperId = '\0vite/preload-helper.js'
3939
const preloadMarkerRE = new RegExp('\\b' + preloadMarker + '\\b', 'g')
@@ -93,6 +93,9 @@ function preload(
9393

9494
promise = Promise.allSettled(
9595
deps.map((dep) => {
96+
// @ts-expect-error chunkRegistry is declared before preload.toString()
97+
dep = chunkRegistry[dep]
98+
9699
// @ts-expect-error assetsURL is declared before preload.toString()
97100
dep = assetsURL(dep, importerUrl)
98101
if (dep in seen) return
@@ -211,6 +214,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
211214
// is appended inside __vitePreload too.
212215
`(dep) => ${JSON.stringify(config.base)}+dep`
213216
const code = [
217+
`const chunkRegistry = ${chunkRegistryPlaceholder}`,
214218
`const scriptRel = ${scriptRel}`,
215219
`const assetsURL = ${assetsURL}`,
216220
`const seen = {}`,
@@ -381,25 +385,31 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
381385
},
382386

383387
renderChunk(code, _, { format }) {
388+
const s = new MagicString(code)
389+
384390
// make sure we only perform the preload logic in modern builds.
385-
if (!code.includes(isEsmFlag)) {
386-
return
391+
if (code.includes(isEsmFlag)) {
392+
const isEsm = String(format === 'es')
393+
let match: RegExpExecArray | null
394+
while ((match = isEsmFlagPattern.exec(code))) {
395+
s.update(match.index, match.index + isEsmFlag.length, isEsm)
396+
}
387397
}
388398

389-
const re = new RegExp(isEsmFlag, 'g')
390-
const isEsm = String(format === 'es')
391-
if (!this.environment.config.build.sourcemap) {
392-
return code.replace(re, isEsm)
399+
if (format !== 'es' && code.includes(chunkRegistryPlaceholder)) {
400+
s.overwrite(
401+
code.indexOf(chunkRegistryPlaceholder),
402+
code.indexOf(chunkRegistryPlaceholder) +
403+
chunkRegistryPlaceholder.length,
404+
'""',
405+
)
393406
}
394407

395-
const s = new MagicString(code)
396-
let match: RegExpExecArray | null
397-
while ((match = re.exec(code))) {
398-
s.update(match.index, match.index + isEsmFlag.length, isEsm)
399-
}
400408
return {
401409
code: s.toString(),
402-
map: s.generateMap({ hires: 'boundary' }),
410+
map: this.environment.config.build.sourcemap
411+
? s.generateMap({ hires: 'boundary' })
412+
: null,
403413
}
404414
},
405415

@@ -469,6 +479,15 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
469479
const buildSourcemap = this.environment.config.build.sourcemap
470480
const { modulePreload } = this.environment.config.build
471481

482+
const chunkRegistry: string[] = []
483+
const getChunkId = (url: string, runtime: boolean = false) => {
484+
if (!runtime) {
485+
url = JSON.stringify(url)
486+
}
487+
const index = chunkRegistry.indexOf(url)
488+
return index > -1 ? index : chunkRegistry.push(url) - 1
489+
}
490+
472491
for (const chunkName in bundle) {
473492
const chunk = bundle[chunkName]
474493
if (chunk.type !== 'chunk') {
@@ -485,7 +504,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
485504

486505
let dynamicImports!: ImportSpecifier[]
487506
try {
488-
dynamicImports = parseImports(code)[0].filter((i) => i.d > -1)
507+
dynamicImports = parseImports(code)[0].filter((i) => i.d !== -1)
489508
} catch (e: any) {
490509
const loc = numberToPos(code, e.idx)
491510
this.error({
@@ -505,15 +524,6 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
505524
const s = new MagicString(code)
506525
const rewroteMarkerStartPos = new Set() // position of the leading double quote
507526

508-
const chunkRegistry: FileDep[] = []
509-
const getChunkId = (url: string, runtime: boolean = false) => {
510-
const index = chunkRegistry.findIndex((dep) => dep.url === url)
511-
if (index === -1) {
512-
return chunkRegistry.push({ url, runtime }) - 1
513-
}
514-
return index
515-
}
516-
517527
for (const dynamicImport of dynamicImports) {
518528
// To handle escape sequences in specifier strings, the .n field will be provided where possible.
519529
const { s: start, e: end, ss: expStart, se: expEnd } = dynamicImport
@@ -642,31 +652,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
642652
s.update(
643653
markerStartPos,
644654
markerStartPos + preloadMarker.length,
645-
chunkDependencies.length > 0
646-
? `${viteMapDeps}([${chunkDependencies.join(',')}])`
647-
: `[]`,
655+
`[${chunkDependencies.join(',')}]`,
648656
)
649657
rewroteMarkerStartPos.add(markerStartPos)
650658
}
651659
}
652660

653-
if (chunkRegistry.length > 0) {
654-
const chunkRegistryCode = `[${chunkRegistry
655-
.map((fileDep) =>
656-
fileDep.runtime ? fileDep.url : JSON.stringify(fileDep.url),
657-
)
658-
.join(',')}]`
659-
660-
const mapDepsCode = `const ${viteMapDeps}=(i,m=${viteMapDeps},d=(m.f||(m.f=${chunkRegistryCode})))=>i.map(i=>d[i]);\n`
661-
662-
// inject extra code at the top or next line of hashbang
663-
if (code.startsWith('#!')) {
664-
s.prependLeft(code.indexOf('\n') + 1, mapDepsCode)
665-
} else {
666-
s.prepend(mapDepsCode)
667-
}
668-
}
669-
670661
// there may still be markers due to inlined dynamic imports, remove
671662
// all the markers regardless
672663
let markerStartPos = indexOfRegexp(code, preloadMarkerRE)
@@ -685,27 +676,45 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
685676
)
686677
}
687678

688-
if (!s.hasChanged()) {
689-
continue
679+
if (s.hasChanged()) {
680+
patchChunkWithMagicString(chunk, s)
690681
}
682+
}
683+
684+
const chunkToPatchWithRegistry = Object.values(bundle).find(
685+
(chunk) =>
686+
chunk.type === 'chunk' &&
687+
chunk.code.includes(chunkRegistryPlaceholder),
688+
) as OutputChunk | undefined
689+
if (chunkToPatchWithRegistry) {
690+
const chunkRegistryCode = `[${chunkRegistry.join(',')}]`
691+
const s = new MagicString(chunkToPatchWithRegistry.code)
692+
s.overwrite(
693+
chunkToPatchWithRegistry.code.indexOf(chunkRegistryPlaceholder),
694+
chunkToPatchWithRegistry.code.indexOf(chunkRegistryPlaceholder) +
695+
chunkRegistryPlaceholder.length,
696+
chunkRegistryCode,
697+
)
698+
699+
patchChunkWithMagicString(chunkToPatchWithRegistry, s)
700+
}
691701

702+
function patchChunkWithMagicString(chunk: OutputChunk, s: MagicString) {
692703
chunk.code = s.toString()
693704

694705
if (!buildSourcemap || !chunk.map) {
695-
continue
706+
return
696707
}
697708

698-
const nextMap = s.generateMap({
699-
source: chunk.fileName,
700-
hires: 'boundary',
701-
})
709+
const { debugId } = chunk.map
702710
const map = combineSourcemaps(chunk.fileName, [
703-
nextMap as RawSourceMap,
711+
s.generateMap({
712+
source: chunk.fileName,
713+
hires: 'boundary',
714+
}) as RawSourceMap,
704715
chunk.map as RawSourceMap,
705716
]) as SourceMap
706717
map.toUrl = () => genSourceMapUrl(map)
707-
708-
const originalDebugId = chunk.map.debugId
709718
chunk.map = map
710719

711720
if (buildSourcemap === 'inline') {
@@ -715,8 +724,8 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
715724
)
716725
chunk.code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}`
717726
} else {
718-
if (originalDebugId) {
719-
map.debugId = originalDebugId
727+
if (debugId) {
728+
map.debugId = debugId
720729
}
721730
const mapAsset = bundle[chunk.fileName + '.map']
722731
if (mapAsset && mapAsset.type === 'asset') {

playground/js-sourcemap/__tests__/js-sourcemap.spec.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ describe.runIf(isBuild)('build tests', () => {
143143
{
144144
"debugId": "00000000-0000-0000-0000-000000000000",
145145
"ignoreList": [],
146-
"mappings": ";+7BAAA,OAAO,2BAAuB,0BAE9B,QAAQ,IAAI,uBAAuB",
146+
"mappings": "6gCAAA,OAAO,2BAAuB,SAE9B,QAAQ,IAAI,uBAAuB",
147147
"sources": [
148148
"../../after-preload-dynamic.js",
149149
],
@@ -163,19 +163,6 @@ describe.runIf(isBuild)('build tests', () => {
163163
)
164164
})
165165

166-
test('__vite__mapDeps injected after banner', async () => {
167-
const js = findAssetFile(/after-preload-dynamic-hashbang-[-\w]{8}\.js$/)
168-
expect(js.split('\n').slice(0, 2)).toEqual([
169-
'#!/usr/bin/env node',
170-
expect.stringContaining('const __vite__mapDeps=(i'),
171-
])
172-
})
173-
174-
test('no unused __vite__mapDeps', async () => {
175-
const js = findAssetFile(/after-preload-dynamic-no-dep-[-\w]{8}\.js$/)
176-
expect(js).not.toMatch(/__vite__mapDeps/)
177-
})
178-
179166
test('sourcemap is correct when using object as "define" value', async () => {
180167
const map = findAssetFile(/with-define-object.*\.js\.map/)
181168
expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(`

0 commit comments

Comments
 (0)