mirror of
https://github.com/nuxt/nuxt.git
synced 2024-12-04 19:37:18 +00:00
perf(nuxt): preload app manifest (#30017)
This commit is contained in:
parent
3317b63557
commit
a01c41b4d3
@ -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 }),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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 })
|
||||
|
@ -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)))),
|
||||
],
|
||||
|
@ -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 }
|
||||
|
@ -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"`)
|
||||
|
Loading…
Reference in New Issue
Block a user