Skip to content

Commit cf176dd

Browse files
authored
feat: allow precomputing dependency graph (#270)
1 parent fe37a3d commit cf176dd

File tree

5 files changed

+494
-58
lines changed

5 files changed

+494
-58
lines changed

benchmark/renderer.bench.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,54 @@
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', () => {
11+
// Precompute dependencies for benchmarks
12+
const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest))
13+
const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest))
14+
const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest))
15+
1016
bench('vite', () => {
17+
createRenderer(() => ({}), {
18+
precomputed: vitePrecomputed,
19+
renderToString: () => '<div>test</div>',
20+
})
21+
})
22+
23+
bench('vite (manifest)', () => {
1124
createRenderer(() => ({}), {
1225
manifest: normalizeViteManifest(viteManifest),
1326
renderToString: () => '<div>test</div>',
1427
})
1528
})
1629

1730
bench('webpack', () => {
31+
createRenderer(() => ({}), {
32+
precomputed: webpackPrecomputed,
33+
renderToString: () => '<div>test</div>',
34+
})
35+
})
36+
37+
bench('webpack (manifest)', () => {
1838
createRenderer(() => ({}), {
1939
manifest: normalizeWebpackManifest(webpackManifest),
2040
renderToString: () => '<div>test</div>',
2141
})
2242
})
2343

2444
bench('vite (large)', () => {
45+
createRenderer(() => ({}), {
46+
precomputed: largeVitePrecomputed,
47+
renderToString: () => '<div>test</div>',
48+
})
49+
})
50+
51+
bench('vite (large) (manifest)', () => {
2552
createRenderer(() => ({}), {
2653
manifest: normalizeViteManifest(largeViteManifest),
2754
renderToString: () => '<div>test</div>',
@@ -30,6 +57,12 @@ describe('createRenderer', () => {
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)
@@ -59,50 +108,98 @@ describe('rendering', () => {
59108
const largeLargeViteSet = new Set(largeViteModules.slice(0, 50))
60109

61110
bench('renderStyles - vite (small)', () => {
111+
renderStyles({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
112+
})
113+
114+
bench('renderStyles - vite (small) (manifest)', () => {
62115
renderStyles({ modules: smallViteSet }, viteRenderer.rendererContext)
63116
})
64117

65118
bench('renderStyles - vite (large)', () => {
119+
renderStyles({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
120+
})
121+
122+
bench('renderStyles - vite (large) (manifest)', () => {
66123
renderStyles({ modules: largeViteSet }, viteRenderer.rendererContext)
67124
})
68125

69126
bench('renderStyles - vite (very large)', () => {
127+
renderStyles({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
128+
})
129+
130+
bench('renderStyles - vite (very large) (manifest)', () => {
70131
renderStyles({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
71132
})
72133

73134
bench('renderScripts - vite (small)', () => {
135+
renderScripts({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
136+
})
137+
138+
bench('renderScripts - vite (small) (manifest)', () => {
74139
renderScripts({ modules: smallViteSet }, viteRenderer.rendererContext)
75140
})
76141

77142
bench('renderScripts - vite (large)', () => {
143+
renderScripts({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
144+
})
145+
146+
bench('renderScripts - vite (large) (manifest)', () => {
78147
renderScripts({ modules: largeViteSet }, viteRenderer.rendererContext)
79148
})
80149

81150
bench('renderScripts - vite (very large)', () => {
151+
renderScripts({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
152+
})
153+
154+
bench('renderScripts - vite (very large) (manifest)', () => {
82155
renderScripts({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
83156
})
84157

85158
bench('renderResourceHints - vite (small)', () => {
159+
renderResourceHints({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
160+
})
161+
162+
bench('renderResourceHints - vite (small) (manifest)', () => {
86163
renderResourceHints({ modules: smallViteSet }, viteRenderer.rendererContext)
87164
})
88165

89166
bench('renderResourceHints - vite (large)', () => {
167+
renderResourceHints({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
168+
})
169+
170+
bench('renderResourceHints - vite (large) (manifest)', () => {
90171
renderResourceHints({ modules: largeViteSet }, viteRenderer.rendererContext)
91172
})
92173

93174
bench('renderResourceHints - vite (very large)', () => {
175+
renderResourceHints({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
176+
})
177+
178+
bench('renderResourceHints - vite (very large) (manifest)', () => {
94179
renderResourceHints({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
95180
})
96181

97182
bench('renderStyles - webpack', () => {
183+
renderStyles({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
184+
})
185+
186+
bench('renderStyles - webpack (manifest)', () => {
98187
renderStyles({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
99188
})
100189

101190
bench('renderScripts - webpack', () => {
191+
renderScripts({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
192+
})
193+
194+
bench('renderScripts - webpack (manifest)', () => {
102195
renderScripts({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
103196
})
104197

105198
bench('renderResourceHints - webpack', () => {
199+
renderResourceHints({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
200+
})
201+
202+
bench('renderResourceHints - webpack (manifest)', () => {
106203
renderResourceHints({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
107204
})
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)