From 22ace7b075276c340d0ae2a1f260d119e82c6470 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 13 Mar 2023 13:21:40 +0800 Subject: [PATCH] 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. --- src/node/build/build.ts | 15 ++++++++++----- src/node/build/render.ts | 17 +++++++++++------ src/node/plugin.ts | 22 ++++++---------------- src/node/utils/fnSerialize.ts | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 3efdbf8c..c564b618 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -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 ) ) ) diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 4b7dbd74..15fdcce9 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -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, @@ -26,7 +27,8 @@ export async function renderPage( appChunk: OutputChunk | undefined, cssChunk: OutputAsset | undefined, pageToHashMap: Record, - 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 = ` @@ -160,11 +169,7 @@ export async function renderPage( ${teleports?.body || ''}
${content}
- ${ - config.mpa - ? '' - : `` - } + ${config.mpa ? '' : ``} ${ appChunk ? `` diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 06954fc6..50f7f218 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -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 - } -} diff --git a/src/node/utils/fnSerialize.ts b/src/node/utils/fnSerialize.ts index 6b87035e..78e7d78f 100644 --- a/src/node/utils/fnSerialize.ts +++ b/src/node/utils/fnSerialize.ts @@ -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 + } +}