perf: inline site data on page

Previously the site data is bundled in JavaScript, and when it changes,
it invalidates the chunk hash of the framework chunk, and in turn
invlidates the chunk hash of **every page**.

With this change, the site data is now inlined in each page as a JSON
string, similar to the page chunk hash data. This ensures that config
changes will no longer affect JavaScript chunk hashes.
pull/2040/head
Evan You 1 year ago
parent 8b34a4c058
commit 22ace7b075

@ -9,6 +9,7 @@ import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'
import { packageDirectorySync } from 'pkg-dir'
import { serializeFunctions } from '../utils/fnSerialize'
export async function build(
root?: string,
@ -58,11 +59,14 @@ export async function build(
(chunk) => chunk.type === 'asset' && chunk.fileName.endsWith('.css')
) as OutputAsset
// We embed the hash map string into each page directly so that it doesn't
// alter the main chunk's hash on every build. It's also embedded as a
// string and JSON.parsed from the client because it's faster than embedding
// as JS object literal.
// We embed the hash map and site config strings into each page directly
// so that it doesn't alter the main chunk's hash on every build.
// It's also embedded as a string and JSON.parsed from the client because
// it's faster than embedding as JS object literal.
const hashMapString = JSON.stringify(JSON.stringify(pageToHashMap))
const siteDataString = JSON.stringify(
JSON.stringify(serializeFunctions(siteConfig.site))
)
await Promise.all(
['404.md', ...siteConfig.pages]
@ -76,7 +80,8 @@ export async function build(
appChunk,
cssChunk,
pageToHashMap,
hashMapString
hashMapString,
siteDataString
)
)
)

@ -17,6 +17,7 @@ import {
type SSGContext
} from '../shared'
import { slash } from '../utils/slash'
import { deserializeFunctions } from '../utils/fnSerialize'
export async function renderPage(
render: (path: string) => Promise<SSGContext>,
@ -26,7 +27,8 @@ export async function renderPage(
appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>,
hashMapString: string
hashMapString: string,
siteDataString: string
) {
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
@ -145,6 +147,13 @@ export async function renderPage(
}
}
let metadataScript = `__VP_HASH_MAP__ = JSON.parse(${hashMapString})\n`
if (siteDataString.includes('_vp-fn_')) {
metadataScript += `${deserializeFunctions.toString()}\n__VP_SITE_DATA__ = deserializeFunctions(JSON.parse(${siteDataString}))`
} else {
metadataScript += `__VP_SITE_DATA__ = JSON.parse(${siteDataString})`
}
const html = `
<!DOCTYPE html>
<html lang="${siteData.lang}" dir="${siteData.dir}">
@ -160,11 +169,7 @@ export async function renderPage(
</head>
<body>${teleports?.body || ''}
<div id="app">${content}</div>
${
config.mpa
? ''
: `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>`
}
${config.mpa ? '' : `<script>${metadataScript}</script>`}
${
appChunk
? `<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>`

@ -22,7 +22,7 @@ import { staticDataPlugin } from './plugins/staticDataPlugin'
import { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin'
import { serializeFunctions } from './utils/fnSerialize.js'
import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' {
interface UserConfig {
@ -158,6 +158,11 @@ export async function createVitePressPlugin(
// head info is not needed by the client in production build
if (config.command === 'build') {
data = { ...siteData, head: [] }
// in production client build, the data is inlined on each page
// to avoid config changes invalidating every chunk.
if (!ssr) {
return `export default window.__VP_SITE_DATA__`
}
}
data = serializeFunctions(data)
return `${deserializeFunctions.toString()}
@ -359,18 +364,3 @@ export async function createVitePressPlugin(
await dynamicRoutesPlugin(siteConfig)
]
}
function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
acc[key] = deserializeFunctions(value[key])
return acc
}, {} as any)
} else if (typeof value === 'string' && value.startsWith('_vp-fn_')) {
return new Function(`return ${value.slice(7)}`)()
} else {
return value
}
}

@ -12,3 +12,18 @@ export function serializeFunctions(value: any): any {
return value
}
}
export function deserializeFunctions(value: any): any {
if (Array.isArray(value)) {
return value.map(deserializeFunctions)
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).reduce((acc, key) => {
acc[key] = deserializeFunctions(value[key])
return acc
}, {} as any)
} else if (typeof value === 'string' && value.startsWith('_vp-fn_')) {
return new Function(`return ${value.slice(7)}`)()
} else {
return value
}
}

Loading…
Cancel
Save