mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-19 07:51:18 +00:00
perf(nuxt): use browser cache for payloads (#31379)
This commit is contained in:
parent
c0785b1d38
commit
8727e40c59
@ -644,3 +644,20 @@ class SomeClass {
|
||||
const value = new SomeClass().someMethod()
|
||||
// this will return 'decorated'
|
||||
```
|
||||
|
||||
## purgeCachedData
|
||||
|
||||
Nuxt will automatically purge cached data from `useAsyncData` and `nuxtApp.static.data`. This helps prevent memory leaks
|
||||
and ensures fresh data is loaded when needed, but it is possible to disable it:
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
purgeCachedData: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::read-more{icon="i-simple-icons-github" color="gray" to="https://github.com/nuxt/nuxt/pull/31379" target="_blank"}
|
||||
See PR #31379 for implementation details.
|
||||
::
|
||||
|
@ -19,35 +19,43 @@ interface LoadPayloadOptions {
|
||||
/** @since 3.0.0 */
|
||||
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadURL = await _getPayloadURL(url, opts)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const cache = nuxtApp._payloadCache ||= {}
|
||||
if (payloadURL in cache) {
|
||||
return cache[payloadURL] || null
|
||||
// TODO: allow payload extraction for non-prerendered URLs
|
||||
const shouldLoadPayload = await isPrerendered(url)
|
||||
if (!shouldLoadPayload) {
|
||||
return null
|
||||
}
|
||||
cache[payloadURL] = isPrerendered(url).then((prerendered) => {
|
||||
if (!prerendered) {
|
||||
cache[payloadURL] = null
|
||||
return null
|
||||
}
|
||||
return _importPayload(payloadURL).then((payload) => {
|
||||
if (payload) { return payload }
|
||||
|
||||
delete cache[payloadURL]
|
||||
return null
|
||||
})
|
||||
})
|
||||
return cache[payloadURL]
|
||||
const payloadURL = await _getPayloadURL(url, opts)
|
||||
return await _importPayload(payloadURL) || null
|
||||
}
|
||||
let linkRelType: string | undefined
|
||||
function detectLinkRelType () {
|
||||
if (import.meta.server) { return 'preload' }
|
||||
if (linkRelType) { return linkRelType }
|
||||
const relList = document.createElement('link').relList
|
||||
linkRelType = relList && relList.supports && relList.supports('prefetch') ? 'prefetch' : 'preload'
|
||||
return linkRelType
|
||||
}
|
||||
/** @since 3.0.0 */
|
||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
|
||||
nuxtApp.runWithContext(() => useHead({
|
||||
link: [
|
||||
{ rel: 'modulepreload', href: payloadURL },
|
||||
],
|
||||
}))
|
||||
const link = renderJsonPayloads
|
||||
? { rel: detectLinkRelType(), as: 'fetch', crossorigin: 'anonymous', href: payloadURL } as const
|
||||
: { rel: 'modulepreload', crossorigin: '', href: payloadURL } as const
|
||||
|
||||
if (import.meta.server) {
|
||||
nuxtApp.runWithContext(() => useHead({ link: [link] }))
|
||||
} else {
|
||||
const linkEl = document.createElement('link')
|
||||
for (const key of Object.keys(link) as Array<keyof typeof link>) {
|
||||
linkEl[key === 'crossorigin' ? 'crossOrigin' : key] = link[key]!
|
||||
}
|
||||
document.head.appendChild(linkEl)
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
linkEl.addEventListener('load', () => resolve())
|
||||
linkEl.addEventListener('error', () => reject())
|
||||
})
|
||||
}
|
||||
})
|
||||
if (import.meta.server) {
|
||||
onServerPrefetch(() => promise)
|
||||
@ -73,7 +81,7 @@ async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
||||
async function _importPayload (payloadURL: string) {
|
||||
if (import.meta.server || !payloadExtraction) { return null }
|
||||
const payloadPromise = renderJsonPayloads
|
||||
? fetch(payloadURL).then(res => res.text().then(parsePayload))
|
||||
? fetch(payloadURL, { cache: 'force-cache' }).then(res => res.text().then(parsePayload))
|
||||
: import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
||||
|
||||
try {
|
||||
|
@ -154,8 +154,6 @@ interface _NuxtApp {
|
||||
|
||||
/** @internal */
|
||||
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
||||
/** @internal */
|
||||
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any> | null>
|
||||
|
||||
/** @internal */
|
||||
_appConfig: AppConfig
|
||||
|
@ -3,8 +3,9 @@ import { loadPayload } from '../composables/payload'
|
||||
import { onNuxtReady } from '../composables/ready'
|
||||
import { useRouter } from '../composables/router'
|
||||
import { getAppManifest } from '../composables/manifest'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
import { appManifest as isAppManifestEnabled, purgeCachedData } from '#build/nuxt.config.mjs'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:payload',
|
||||
@ -13,11 +14,22 @@ export default defineNuxtPlugin({
|
||||
if (import.meta.dev) { return }
|
||||
|
||||
// Load payload after middleware & once final route is resolved
|
||||
const staticKeysToRemove = new Set<string>()
|
||||
useRouter().beforeResolve(async (to, from) => {
|
||||
if (to.path === from.path) { return }
|
||||
const payload = await loadPayload(to.path)
|
||||
if (!payload) { return }
|
||||
Object.assign(nuxtApp.static.data, payload.data)
|
||||
for (const key of staticKeysToRemove) {
|
||||
if (purgeCachedData) {
|
||||
delete nuxtApp.static.data[key]
|
||||
}
|
||||
}
|
||||
for (const key in payload.data) {
|
||||
if (!(key in nuxtApp.static.data)) {
|
||||
staticKeysToRemove.add(key)
|
||||
}
|
||||
nuxtApp.static.data[key] = payload.data[key]
|
||||
}
|
||||
})
|
||||
|
||||
onNuxtReady(() => {
|
||||
@ -25,7 +37,8 @@ export default defineNuxtPlugin({
|
||||
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
||||
const { hostname } = new URL(url, window.location.href)
|
||||
if (hostname === window.location.hostname) {
|
||||
await loadPayload(url)
|
||||
// TODO: use preloadPayload instead once we can support preloading islands too
|
||||
await loadPayload(url).catch(() => { console.warn('[nuxt] Error preloading payload for', url) })
|
||||
}
|
||||
})
|
||||
if (isAppManifestEnabled && navigator.connection?.effectiveType !== 'slow-2g') {
|
||||
|
@ -562,6 +562,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
|
||||
`export const crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
|
||||
`export const spaLoadingTemplateOutside = ${ctx.nuxt.options.experimental.spaLoadingTemplateLocation === 'body'}`,
|
||||
`export const purgeCachedData = ${!!ctx.nuxt.options.experimental.purgeCachedData}`,
|
||||
].join('\n\n')
|
||||
},
|
||||
}
|
||||
|
@ -723,5 +723,30 @@ export default defineResolvers({
|
||||
* @see [PR #31175](https://github.com/nuxt/nuxt/pull/31175)
|
||||
*/
|
||||
templateImportResolution: true,
|
||||
|
||||
/**
|
||||
* Whether to clean up Nuxt static and asyncData caches on route navigation.
|
||||
*
|
||||
* Nuxt will automatically purge cached data from `useAsyncData` and `nuxtApp.static.data`. This helps prevent memory leaks
|
||||
* and ensures fresh data is loaded when needed, but it is possible to disable it.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // nuxt.config.ts
|
||||
* export default defineNuxtConfig({
|
||||
* experimental: {
|
||||
* // Disable automatic cache cleanup (default is true)
|
||||
* purgeCachedData: false
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @see [PR #31379](https://github.com/nuxt/nuxt/pull/31379)
|
||||
*/
|
||||
purgeCachedData: {
|
||||
$resolve: (val) => {
|
||||
return typeof val === 'boolean' ? val : true
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -2079,13 +2079,14 @@ describe.skipIf(isDev() || isWindows || !isRenderingJson)('prefetching', () => {
|
||||
await gotoPath(page, '/prefetch')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const snapshot = [...requests]
|
||||
expect(requests.some(req => req.startsWith('/__nuxt_island/AsyncServerComponent'))).toBe(true)
|
||||
requests.length = 0
|
||||
await page.click('[href="/prefetch/server-components"]')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
expect(await page.innerHTML('#async-server-component-count')).toBe('34')
|
||||
|
||||
expect(requests).toEqual(snapshot)
|
||||
expect(requests.some(req => req.startsWith('/__nuxt_island/AsyncServerComponent'))).toBe(false)
|
||||
await page.close()
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user