Skip to content

Commit bd64eb2

Browse files
committed
perf: allow precomputing dependency graph
1 parent fe37a3d commit bd64eb2

File tree

5 files changed

+354
-25
lines changed

5 files changed

+354
-25
lines changed

benchmark/renderer.bench.ts

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,68 @@
11
import { bench, describe } from 'vitest'
22
import { createRenderer, renderStyles, renderScripts, renderResourceHints } from '../src/runtime'
33
import { normalizeViteManifest, normalizeWebpackManifest } from '../src'
4+
import { precomputeDependencies } from '../src/precompute'
45

56
import viteManifest from '../test/fixtures/vite-manifest.json'
67
import webpackManifest from '../test/fixtures/webpack-manifest.json'
78
import largeViteManifest from './fixtures/large-vite-manifest.json'
89

910
describe('createRenderer', () => {
10-
bench('vite', () => {
11+
// Precompute dependencies for benchmarks
12+
const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest))
13+
const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest))
14+
const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest))
15+
16+
bench('vite (manifest)', () => {
1117
createRenderer(() => ({}), {
1218
manifest: normalizeViteManifest(viteManifest),
1319
renderToString: () => '<div>test</div>',
1420
})
1521
})
1622

17-
bench('webpack', () => {
23+
bench('vite (precomputed)', () => {
24+
createRenderer(() => ({}), {
25+
precomputed: vitePrecomputed,
26+
renderToString: () => '<div>test</div>',
27+
})
28+
})
29+
30+
bench('webpack (manifest)', () => {
1831
createRenderer(() => ({}), {
1932
manifest: normalizeWebpackManifest(webpackManifest),
2033
renderToString: () => '<div>test</div>',
2134
})
2235
})
2336

24-
bench('vite (large)', () => {
37+
bench('webpack (precomputed)', () => {
38+
createRenderer(() => ({}), {
39+
precomputed: webpackPrecomputed,
40+
renderToString: () => '<div>test</div>',
41+
})
42+
})
43+
44+
bench('vite large (manifest)', () => {
2545
createRenderer(() => ({}), {
2646
manifest: normalizeViteManifest(largeViteManifest),
2747
renderToString: () => '<div>test</div>',
2848
})
2949
})
50+
51+
bench('vite large (precomputed)', () => {
52+
createRenderer(() => ({}), {
53+
precomputed: largeVitePrecomputed,
54+
renderToString: () => '<div>test</div>',
55+
})
56+
})
3057
})
3158

3259
describe('rendering', () => {
60+
// Precompute dependencies
61+
const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest))
62+
const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest))
63+
const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest))
64+
65+
// Legacy renderers (with manifest)
3366
const viteRenderer = createRenderer(() => ({}), {
3467
manifest: normalizeViteManifest(viteManifest),
3568
renderToString: () => '<div>test</div>',
@@ -45,6 +78,22 @@ describe('rendering', () => {
4578
renderToString: () => '<div>test</div>',
4679
})
4780

81+
// Precomputed renderers
82+
const vitePrecomputedRenderer = createRenderer(() => ({}), {
83+
precomputed: vitePrecomputed,
84+
renderToString: () => '<div>test</div>',
85+
})
86+
87+
const webpackPrecomputedRenderer = createRenderer(() => ({}), {
88+
precomputed: webpackPrecomputed,
89+
renderToString: () => '<div>test</div>',
90+
})
91+
92+
const largeVitePrecomputedRenderer = createRenderer(() => ({}), {
93+
precomputed: largeVitePrecomputed,
94+
renderToString: () => '<div>test</div>',
95+
})
96+
4897
// Get actual module keys from manifests
4998
const viteModules = Object.keys(viteManifest)
5099
const webpackModules = Object.keys(webpackManifest)
@@ -58,51 +107,99 @@ describe('rendering', () => {
58107
const largeViteSet = new Set(viteModules)
59108
const largeLargeViteSet = new Set(largeViteModules.slice(0, 50))
60109

61-
bench('renderStyles - vite (small)', () => {
110+
bench('renderStyles - vite small (manifest)', () => {
62111
renderStyles({ modules: smallViteSet }, viteRenderer.rendererContext)
63112
})
64113

65-
bench('renderStyles - vite (large)', () => {
114+
bench('renderStyles - vite small (precomputed)', () => {
115+
renderStyles({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
116+
})
117+
118+
bench('renderStyles - vite large (manifest)', () => {
66119
renderStyles({ modules: largeViteSet }, viteRenderer.rendererContext)
67120
})
68121

69-
bench('renderStyles - vite (very large)', () => {
122+
bench('renderStyles - vite large (precomputed)', () => {
123+
renderStyles({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
124+
})
125+
126+
bench('renderStyles - vite very large (manifest)', () => {
70127
renderStyles({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
71128
})
72129

73-
bench('renderScripts - vite (small)', () => {
130+
bench('renderStyles - vite very large (precomputed)', () => {
131+
renderStyles({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
132+
})
133+
134+
bench('renderScripts - vite small (manifest)', () => {
74135
renderScripts({ modules: smallViteSet }, viteRenderer.rendererContext)
75136
})
76137

77-
bench('renderScripts - vite (large)', () => {
138+
bench('renderScripts - vite small (precomputed)', () => {
139+
renderScripts({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
140+
})
141+
142+
bench('renderScripts - vite large (manifest)', () => {
78143
renderScripts({ modules: largeViteSet }, viteRenderer.rendererContext)
79144
})
80145

81-
bench('renderScripts - vite (very large)', () => {
146+
bench('renderScripts - vite large (precomputed)', () => {
147+
renderScripts({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
148+
})
149+
150+
bench('renderScripts - vite very large (manifest)', () => {
82151
renderScripts({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
83152
})
84153

85-
bench('renderResourceHints - vite (small)', () => {
154+
bench('renderScripts - vite very large (precomputed)', () => {
155+
renderScripts({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
156+
})
157+
158+
bench('renderResourceHints - vite small (manifest)', () => {
86159
renderResourceHints({ modules: smallViteSet }, viteRenderer.rendererContext)
87160
})
88161

89-
bench('renderResourceHints - vite (large)', () => {
162+
bench('renderResourceHints - vite small (precomputed)', () => {
163+
renderResourceHints({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
164+
})
165+
166+
bench('renderResourceHints - vite large (manifest)', () => {
90167
renderResourceHints({ modules: largeViteSet }, viteRenderer.rendererContext)
91168
})
92169

93-
bench('renderResourceHints - vite (very large)', () => {
170+
bench('renderResourceHints - vite large (precomputed)', () => {
171+
renderResourceHints({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
172+
})
173+
174+
bench('renderResourceHints - vite very large (manifest)', () => {
94175
renderResourceHints({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
95176
})
96177

97-
bench('renderStyles - webpack', () => {
178+
bench('renderResourceHints - vite very large (precomputed)', () => {
179+
renderResourceHints({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
180+
})
181+
182+
bench('renderStyles - webpack (manifest)', () => {
98183
renderStyles({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
99184
})
100185

101-
bench('renderScripts - webpack', () => {
186+
bench('renderStyles - webpack (precomputed)', () => {
187+
renderStyles({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
188+
})
189+
190+
bench('renderScripts - webpack (manifest)', () => {
102191
renderScripts({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
103192
})
104193

105-
bench('renderResourceHints - webpack', () => {
194+
bench('renderScripts - webpack (precomputed)', () => {
195+
renderScripts({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
196+
})
197+
198+
bench('renderResourceHints - webpack (manifest)', () => {
106199
renderResourceHints({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
107200
})
201+
202+
bench('renderResourceHints - webpack (precomputed)', () => {
203+
renderResourceHints({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
204+
})
108205
})

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export { normalizeViteManifest } from './vite'
22
export { normalizeWebpackManifest } from './webpack'
3+
export { precomputeDependencies } from './precompute'
4+
export type { PrecomputedData } from './precompute'
35

46
export * from './types'

src/precompute.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { Manifest, ResourceMeta } from './types'
2+
import type { ModuleDependencies } from './runtime'
3+
4+
export interface PrecomputedData {
5+
/** Pre-resolved dependencies for each module */
6+
dependencies: Record<string, ModuleDependencies>
7+
/** List of entry point module IDs */
8+
entrypoints: string[]
9+
/** Module metadata needed at runtime (file paths, etc.) */
10+
modules: Record<string, Pick<ResourceMeta, 'file' | 'resourceType' | 'mimeType' | 'module'>>
11+
}
12+
13+
/**
14+
* Build-time utility to precompute all module dependencies from a manifest.
15+
* This eliminates recursive dependency resolution at runtime.
16+
*
17+
* @param manifest The build manifest
18+
* @returns Serializable precomputed data for runtime use
19+
*/
20+
export function precomputeDependencies(manifest: Manifest): PrecomputedData {
21+
const dependencies: Record<string, ModuleDependencies> = {}
22+
const computing = new Set<string>()
23+
24+
function computeDependencies(id: string): ModuleDependencies {
25+
if (dependencies[id]) {
26+
return dependencies[id]
27+
}
28+
29+
if (computing.has(id)) {
30+
// Circular dependency detected, return empty to break cycle
31+
return { scripts: {}, styles: {}, preload: {}, prefetch: {} }
32+
}
33+
34+
computing.add(id)
35+
36+
const deps: ModuleDependencies = {
37+
scripts: {},
38+
styles: {},
39+
preload: {},
40+
prefetch: {},
41+
}
42+
43+
const meta = manifest[id]
44+
if (!meta) {
45+
dependencies[id] = deps
46+
computing.delete(id)
47+
return deps
48+
}
49+
50+
// Add to scripts + preload
51+
if (meta.file) {
52+
deps.preload[id] = meta
53+
if (meta.isEntry || meta.sideEffects) {
54+
deps.scripts[id] = meta
55+
}
56+
}
57+
58+
// Add styles + preload
59+
for (const css of meta.css || []) {
60+
const cssResource = manifest[css]
61+
if (cssResource) {
62+
deps.styles[css] = cssResource
63+
deps.preload[css] = cssResource
64+
deps.prefetch[css] = cssResource
65+
}
66+
}
67+
68+
// Add assets as preload
69+
for (const asset of meta.assets || []) {
70+
const assetResource = manifest[asset]
71+
if (assetResource) {
72+
deps.preload[asset] = assetResource
73+
deps.prefetch[asset] = assetResource
74+
}
75+
}
76+
77+
// Resolve nested dependencies and merge
78+
for (const depId of meta.imports || []) {
79+
const depDeps = computeDependencies(depId)
80+
Object.assign(deps.styles, depDeps.styles)
81+
Object.assign(deps.preload, depDeps.preload)
82+
Object.assign(deps.prefetch, depDeps.prefetch)
83+
}
84+
85+
// Filter preload based on preload flag
86+
const filteredPreload: ModuleDependencies['preload'] = {}
87+
for (const depId in deps.preload) {
88+
const dep = deps.preload[depId]
89+
if (dep.preload) {
90+
filteredPreload[depId] = dep
91+
}
92+
}
93+
deps.preload = filteredPreload
94+
95+
dependencies[id] = deps
96+
computing.delete(id)
97+
return deps
98+
}
99+
100+
// Pre-compute dependencies for all modules in manifest
101+
for (const moduleId of Object.keys(manifest)) {
102+
computeDependencies(moduleId)
103+
}
104+
105+
// Extract entry points
106+
const entrypoints = new Set<string>()
107+
for (const key in manifest) {
108+
const meta = manifest[key]
109+
if (meta?.isEntry) {
110+
entrypoints.add(key)
111+
}
112+
}
113+
114+
// Extract minimal module metadata needed at runtime
115+
const modules: Record<string, Pick<ResourceMeta, 'file' | 'resourceType' | 'mimeType' | 'module'>> = {}
116+
for (const [moduleId, meta] of Object.entries(manifest)) {
117+
modules[moduleId] = {
118+
file: meta.file,
119+
resourceType: meta.resourceType,
120+
mimeType: meta.mimeType,
121+
module: meta.module,
122+
}
123+
}
124+
125+
return {
126+
dependencies,
127+
entrypoints: [...entrypoints],
128+
modules,
129+
}
130+
}

0 commit comments

Comments
 (0)