perf(nuxt): preload app manifest (#30017)

This commit is contained in:
Harlan Wilton 2024-12-04 00:07:36 +11:00 committed by GitHub
parent 3317b63557
commit a01c41b4d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 12 deletions

View File

@ -1,7 +1,7 @@
import type { MatcherExport, RouteMatcher } from 'radix3'
import { createMatcherFromExport, createRouter as createRadixRouter, toRouteMatcher } from 'radix3'
import { defu } from 'defu'
import { useRuntimeConfig } from '../nuxt'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
// @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file
@ -24,9 +24,14 @@ function fetchManifest () {
if (!isAppManifestEnabled) {
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
}
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`), {
responseType: 'json',
})
if (import.meta.server) {
// @ts-expect-error virtual file
manifest = import('#app-manifest')
} else {
manifest = $fetch<NuxtAppManifest>(buildAssetsURL(`builds/meta/${useRuntimeConfig().app.buildId}.json`), {
responseType: 'json',
})
}
manifest.then((m) => {
matcher = createMatcherFromExport(m.matcher)
}).catch((e) => {
@ -40,12 +45,16 @@ export function getAppManifest (): Promise<NuxtAppManifest> {
if (!isAppManifestEnabled) {
throw new Error('[nuxt] app manifest should be enabled with `experimental.appManifest`')
}
if (import.meta.server) {
useNuxtApp().ssrContext!._preloadManifest = true
}
return manifest || fetchManifest()
}
/** @since 3.7.4 */
export async function getRouteRules (url: string) {
if (import.meta.server) {
useNuxtApp().ssrContext!._preloadManifest = true
const _routeRulesMatcher = toRouteMatcher(
createRadixRouter({ routes: useRuntimeConfig().nitro!.routeRules }),
)

View File

@ -85,15 +85,18 @@ async function _importPayload (payloadURL: string) {
}
/** @since 3.0.0 */
export async function isPrerendered (url = useRoute().path) {
const nuxtApp = useNuxtApp()
// Note: Alternative for server is checking x-nitro-prerender header
if (!appManifest) { return !!useNuxtApp().payload.prerenderedAt }
if (!appManifest) { return !!nuxtApp.payload.prerenderedAt }
url = withoutTrailingSlash(url)
const manifest = await getAppManifest()
if (manifest.prerendered.includes(url)) {
return true
}
const rules = await getRouteRules(url)
return !!rules.prerender && !rules.redirect
return nuxtApp.runWithContext(async () => {
const rules = await getRouteRules(url)
return !!rules.prerender && !rules.redirect
})
}
let payloadCache: NuxtPayload | null = null

View File

@ -81,6 +81,8 @@ export interface NuxtSSRContext extends SSRContext {
get<T = unknown> (key: string): Promise<T> | undefined
set<T> (key: string, value: Promise<T>): Promise<void>
}
/** @internal */
_preloadManifest?: boolean
}
export interface NuxtPayload {

View File

@ -273,7 +273,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nuxt.options.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
// write stub manifest before build so external import of #app-manifest can be resolved
if (!nuxt.options.dev) {
nuxt.hook('build:before', async () => {
await fsp.mkdir(join(tempDir, 'meta'), { recursive: true })
await fsp.writeFile(join(tempDir, `meta/${buildId}.json`), JSON.stringify({}))
})
}
nuxt.hook('nitro:config', (config) => {
config.alias ||= {}
config.alias['#app-manifest'] = join(tempDir, `meta/${buildId}.json`)
const rules = config.routeRules
for (const rule in rules) {
if (!(rules[rule] as any).appMiddleware) { continue }
@ -349,6 +360,11 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
})
}
// add stub alias to allow vite to resolve import
if (!nuxt.options.experimental.appManifest) {
nuxt.options.alias['#app-manifest'] = 'unenv/runtime/mock/proxy'
}
// Add fallback server for `ssr: false`
const FORWARD_SLASH_RE = /\//g
if (!nuxt.options.ssr) {

View File

@ -30,7 +30,7 @@ import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
import type { NuxtPayload, NuxtSSRContext } from '#app'
// @ts-expect-error virtual file
import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, multiApp } from '#internal/nuxt.config.mjs'
import { appHead, appId, appRootAttrs, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, appManifest as isAppManifestEnabled, multiApp } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -379,7 +379,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// Setup head
const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext)
// 1.Extracted payload preloading
// 1. Preload payloads and app manifest
if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS && !isRenderingIsland) {
head.push({
link: [
@ -389,7 +389,13 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
],
}, headEntryOptions)
}
if (isAppManifestEnabled && ssrContext._preloadManifest) {
head.push({
link: [
{ rel: 'preload', as: 'fetch', fetchpriority: 'low', crossorigin: 'anonymous', href: buildAssetsURL(`builds/meta/${ssrContext.runtimeConfig.app.buildId}.json`) },
],
}, { ...headEntryOptions, tagPriority: 'low' })
}
// 2. Styles
if (inlinedStyles.length) {
head.push({ style: inlinedStyles })

View File

@ -85,6 +85,7 @@ export async function buildServer (ctx: ViteBuildContext) {
'nitro/runtime',
'#internal/nuxt/paths',
'#internal/nuxt/app-config',
'#app-manifest',
'#shared',
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
],

View File

@ -59,7 +59,7 @@ function serverStandalone (ctx: WebpackConfigContext) {
resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared),
]
if (!ctx.nuxt.options.dev) {
external.push('#internal/nuxt/paths', '#internal/nuxt/app-config')
external.push('#internal/nuxt/paths', '#internal/nuxt/app-config', '#app-manifest')
}
if (!Array.isArray(ctx.config.externals)) { return }

View File

@ -37,7 +37,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"208k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`)