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 2 years ago
parent 8b34a4c058
commit 22ace7b075

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

@ -17,6 +17,7 @@ import {
type SSGContext type SSGContext
} from '../shared' } from '../shared'
import { slash } from '../utils/slash' import { slash } from '../utils/slash'
import { deserializeFunctions } from '../utils/fnSerialize'
export async function renderPage( export async function renderPage(
render: (path: string) => Promise<SSGContext>, render: (path: string) => Promise<SSGContext>,
@ -26,7 +27,8 @@ export async function renderPage(
appChunk: OutputChunk | undefined, appChunk: OutputChunk | undefined,
cssChunk: OutputAsset | undefined, cssChunk: OutputAsset | undefined,
pageToHashMap: Record<string, string>, pageToHashMap: Record<string, string>,
hashMapString: string hashMapString: string,
siteDataString: string
) { ) {
const routePath = `/${page.replace(/\.md$/, '')}` const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath) 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 = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="${siteData.lang}" dir="${siteData.dir}"> <html lang="${siteData.lang}" dir="${siteData.dir}">
@ -160,11 +169,7 @@ export async function renderPage(
</head> </head>
<body>${teleports?.body || ''} <body>${teleports?.body || ''}
<div id="app">${content}</div> <div id="app">${content}</div>
${ ${config.mpa ? '' : `<script>${metadataScript}</script>`}
config.mpa
? ''
: `<script>__VP_HASH_MAP__ = JSON.parse(${hashMapString})</script>`
}
${ ${
appChunk appChunk
? `<script type="module" async src="${siteData.base}${appChunk.fileName}"></script>` ? `<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 { webFontsPlugin } from './plugins/webFontsPlugin'
import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin' import { dynamicRoutesPlugin } from './plugins/dynamicRoutesPlugin'
import { rewritesPlugin } from './plugins/rewritesPlugin' import { rewritesPlugin } from './plugins/rewritesPlugin'
import { serializeFunctions } from './utils/fnSerialize.js' import { serializeFunctions, deserializeFunctions } from './utils/fnSerialize'
declare module 'vite' { declare module 'vite' {
interface UserConfig { interface UserConfig {
@ -158,6 +158,11 @@ export async function createVitePressPlugin(
// head info is not needed by the client in production build // head info is not needed by the client in production build
if (config.command === 'build') { if (config.command === 'build') {
data = { ...siteData, head: [] } 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) data = serializeFunctions(data)
return `${deserializeFunctions.toString()} return `${deserializeFunctions.toString()}
@ -359,18 +364,3 @@ export async function createVitePressPlugin(
await dynamicRoutesPlugin(siteConfig) 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 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