Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions benchmark/renderer.bench.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
import { bench, describe } from 'vitest'
import { createRenderer, renderStyles, renderScripts, renderResourceHints } from '../src/runtime'
import { normalizeViteManifest, normalizeWebpackManifest } from '../src'
import { precomputeDependencies } from '../src/precompute'

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

describe('createRenderer', () => {
// Precompute dependencies for benchmarks
const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest))
const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest))
const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest))

bench('vite', () => {
createRenderer(() => ({}), {
precomputed: vitePrecomputed,
renderToString: () => '<div>test</div>',
})
})

bench('vite (manifest)', () => {
createRenderer(() => ({}), {
manifest: normalizeViteManifest(viteManifest),
renderToString: () => '<div>test</div>',
})
})

bench('webpack', () => {
createRenderer(() => ({}), {
precomputed: webpackPrecomputed,
renderToString: () => '<div>test</div>',
})
})

bench('webpack (manifest)', () => {
createRenderer(() => ({}), {
manifest: normalizeWebpackManifest(webpackManifest),
renderToString: () => '<div>test</div>',
})
})

bench('vite (large)', () => {
createRenderer(() => ({}), {
precomputed: largeVitePrecomputed,
renderToString: () => '<div>test</div>',
})
})

bench('vite (large) (manifest)', () => {
createRenderer(() => ({}), {
manifest: normalizeViteManifest(largeViteManifest),
renderToString: () => '<div>test</div>',
Expand All @@ -30,6 +57,12 @@ describe('createRenderer', () => {
})

describe('rendering', () => {
// Precompute dependencies
const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest))
const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest))
const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest))

// Legacy renderers (with manifest)
const viteRenderer = createRenderer(() => ({}), {
manifest: normalizeViteManifest(viteManifest),
renderToString: () => '<div>test</div>',
Expand All @@ -45,6 +78,22 @@ describe('rendering', () => {
renderToString: () => '<div>test</div>',
})

// Precomputed renderers
const vitePrecomputedRenderer = createRenderer(() => ({}), {
precomputed: vitePrecomputed,
renderToString: () => '<div>test</div>',
})

const webpackPrecomputedRenderer = createRenderer(() => ({}), {
precomputed: webpackPrecomputed,
renderToString: () => '<div>test</div>',
})

const largeVitePrecomputedRenderer = createRenderer(() => ({}), {
precomputed: largeVitePrecomputed,
renderToString: () => '<div>test</div>',
})

// Get actual module keys from manifests
const viteModules = Object.keys(viteManifest)
const webpackModules = Object.keys(webpackManifest)
Expand All @@ -59,50 +108,98 @@ describe('rendering', () => {
const largeLargeViteSet = new Set(largeViteModules.slice(0, 50))

bench('renderStyles - vite (small)', () => {
renderStyles({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderStyles - vite (small) (manifest)', () => {
renderStyles({ modules: smallViteSet }, viteRenderer.rendererContext)
})

bench('renderStyles - vite (large)', () => {
renderStyles({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderStyles - vite (large) (manifest)', () => {
renderStyles({ modules: largeViteSet }, viteRenderer.rendererContext)
})

bench('renderStyles - vite (very large)', () => {
renderStyles({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
})

bench('renderStyles - vite (very large) (manifest)', () => {
renderStyles({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
})

bench('renderScripts - vite (small)', () => {
renderScripts({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderScripts - vite (small) (manifest)', () => {
renderScripts({ modules: smallViteSet }, viteRenderer.rendererContext)
})

bench('renderScripts - vite (large)', () => {
renderScripts({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderScripts - vite (large) (manifest)', () => {
renderScripts({ modules: largeViteSet }, viteRenderer.rendererContext)
})

bench('renderScripts - vite (very large)', () => {
renderScripts({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
})

bench('renderScripts - vite (very large) (manifest)', () => {
renderScripts({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
})

bench('renderResourceHints - vite (small)', () => {
renderResourceHints({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderResourceHints - vite (small) (manifest)', () => {
renderResourceHints({ modules: smallViteSet }, viteRenderer.rendererContext)
})

bench('renderResourceHints - vite (large)', () => {
renderResourceHints({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext)
})

bench('renderResourceHints - vite (large) (manifest)', () => {
renderResourceHints({ modules: largeViteSet }, viteRenderer.rendererContext)
})

bench('renderResourceHints - vite (very large)', () => {
renderResourceHints({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext)
})

bench('renderResourceHints - vite (very large) (manifest)', () => {
renderResourceHints({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext)
})

bench('renderStyles - webpack', () => {
renderStyles({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
})

bench('renderStyles - webpack (manifest)', () => {
renderStyles({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
})

bench('renderScripts - webpack', () => {
renderScripts({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
})

bench('renderScripts - webpack (manifest)', () => {
renderScripts({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
})

bench('renderResourceHints - webpack', () => {
renderResourceHints({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext)
})

bench('renderResourceHints - webpack (manifest)', () => {
renderResourceHints({ modules: smallWebpackSet }, webpackRenderer.rendererContext)
})
})
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { normalizeViteManifest } from './vite'
export { normalizeWebpackManifest } from './webpack'
export { precomputeDependencies } from './precompute'
export type { PrecomputedData } from './precompute'

export * from './types'
130 changes: 130 additions & 0 deletions src/precompute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { Manifest, ResourceMeta } from './types'
import type { ModuleDependencies } from './runtime'

export interface PrecomputedData {
/** Pre-resolved dependencies for each module */
dependencies: Record<string, ModuleDependencies>
/** List of entry point module IDs */
entrypoints: string[]
/** Module metadata needed at runtime (file paths, etc.) */
modules: Record<string, Pick<ResourceMeta, 'file' | 'resourceType' | 'mimeType' | 'module'>>
}

/**
* Build-time utility to precompute all module dependencies from a manifest.
* This eliminates recursive dependency resolution at runtime.
*
* @param manifest The build manifest
* @returns Serializable precomputed data for runtime use
*/
export function precomputeDependencies(manifest: Manifest): PrecomputedData {
const dependencies: Record<string, ModuleDependencies> = {}
const computing = new Set<string>()

function computeDependencies(id: string): ModuleDependencies {
if (dependencies[id]) {
return dependencies[id]
}

if (computing.has(id)) {
// Circular dependency detected, return empty to break cycle
return { scripts: {}, styles: {}, preload: {}, prefetch: {} }
}

computing.add(id)

const deps: ModuleDependencies = {
scripts: {},
styles: {},
preload: {},
prefetch: {},
}

const meta = manifest[id]
if (!meta) {
dependencies[id] = deps
computing.delete(id)
return deps
}

// Add to scripts + preload
if (meta.file) {
deps.preload[id] = meta
if (meta.isEntry || meta.sideEffects) {
deps.scripts[id] = meta
}
}

// Add styles + preload
for (const css of meta.css || []) {
const cssResource = manifest[css]
if (cssResource) {
deps.styles[css] = cssResource
deps.preload[css] = cssResource
deps.prefetch[css] = cssResource
}
}

// Add assets as preload
for (const asset of meta.assets || []) {
const assetResource = manifest[asset]
if (assetResource) {
deps.preload[asset] = assetResource
deps.prefetch[asset] = assetResource
}
}

// Resolve nested dependencies and merge
for (const depId of meta.imports || []) {
const depDeps = computeDependencies(depId)
Object.assign(deps.styles, depDeps.styles)
Object.assign(deps.preload, depDeps.preload)
Object.assign(deps.prefetch, depDeps.prefetch)
}

// Filter preload based on preload flag
const filteredPreload: ModuleDependencies['preload'] = {}
for (const depId in deps.preload) {
const dep = deps.preload[depId]
if (dep.preload) {
filteredPreload[depId] = dep
}
}
deps.preload = filteredPreload

dependencies[id] = deps
computing.delete(id)
return deps
}

// Pre-compute dependencies for all modules in manifest
for (const moduleId of Object.keys(manifest)) {
computeDependencies(moduleId)
}

// Extract entry points
const entrypoints = new Set<string>()
for (const key in manifest) {
const meta = manifest[key]
if (meta?.isEntry) {
entrypoints.add(key)
}
}

// Extract minimal module metadata needed at runtime
const modules: Record<string, Pick<ResourceMeta, 'file' | 'resourceType' | 'mimeType' | 'module'>> = {}
for (const [moduleId, meta] of Object.entries(manifest)) {
modules[moduleId] = {
file: meta.file,
resourceType: meta.resourceType,
mimeType: meta.mimeType,
module: meta.module,
}
}

return {
dependencies,
entrypoints: [...entrypoints],
modules,
}
}
Loading
Loading