mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-20 16:25:55 +00:00
perf(nuxt): use browser cache for payloads (#31379)
This commit is contained in:
parent
8df17ea6ec
commit
a01290b60d
@ -592,3 +592,20 @@ class SomeClass {
|
|||||||
const value = new SomeClass().someMethod()
|
const value = new SomeClass().someMethod()
|
||||||
// this will return 'decorated'
|
// 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 */
|
/** @since 3.0.0 */
|
||||||
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
|
export async function loadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<Record<string, any> | null> {
|
||||||
if (import.meta.server || !payloadExtraction) { return null }
|
if (import.meta.server || !payloadExtraction) { return null }
|
||||||
const payloadURL = await _getPayloadURL(url, opts)
|
// TODO: allow payload extraction for non-prerendered URLs
|
||||||
const nuxtApp = useNuxtApp()
|
const shouldLoadPayload = await isPrerendered(url)
|
||||||
const cache = nuxtApp._payloadCache ||= {}
|
if (!shouldLoadPayload) {
|
||||||
if (payloadURL in cache) {
|
return null
|
||||||
return cache[payloadURL] || null
|
|
||||||
}
|
}
|
||||||
cache[payloadURL] = isPrerendered(url).then((prerendered) => {
|
const payloadURL = await _getPayloadURL(url, opts)
|
||||||
if (!prerendered) {
|
return await _importPayload(payloadURL) || null
|
||||||
cache[payloadURL] = null
|
}
|
||||||
return null
|
let linkRelType: string | undefined
|
||||||
}
|
function detectLinkRelType () {
|
||||||
return _importPayload(payloadURL).then((payload) => {
|
if (import.meta.server) { return 'preload' }
|
||||||
if (payload) { return payload }
|
if (linkRelType) { return linkRelType }
|
||||||
|
const relList = document.createElement('link').relList
|
||||||
delete cache[payloadURL]
|
linkRelType = relList && relList.supports && relList.supports('prefetch') ? 'prefetch' : 'preload'
|
||||||
return null
|
return linkRelType
|
||||||
})
|
|
||||||
})
|
|
||||||
return cache[payloadURL]
|
|
||||||
}
|
}
|
||||||
/** @since 3.0.0 */
|
/** @since 3.0.0 */
|
||||||
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
|
export function preloadPayload (url: string, opts: LoadPayloadOptions = {}): Promise<void> {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
|
const promise = _getPayloadURL(url, opts).then((payloadURL) => {
|
||||||
nuxtApp.runWithContext(() => useHead({
|
const link = renderJsonPayloads
|
||||||
link: [
|
? { rel: detectLinkRelType(), as: 'fetch', crossorigin: 'anonymous', href: payloadURL } as const
|
||||||
{ rel: 'modulepreload', href: payloadURL },
|
: { 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) {
|
if (import.meta.server) {
|
||||||
onServerPrefetch(() => promise)
|
onServerPrefetch(() => promise)
|
||||||
@ -73,7 +81,7 @@ async function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
|
|||||||
async function _importPayload (payloadURL: string) {
|
async function _importPayload (payloadURL: string) {
|
||||||
if (import.meta.server || !payloadExtraction) { return null }
|
if (import.meta.server || !payloadExtraction) { return null }
|
||||||
const payloadPromise = renderJsonPayloads
|
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)
|
: import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).then(r => r.default || r)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -150,8 +150,6 @@ interface _NuxtApp {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
_observer?: { observe: (element: Element, callback: () => void) => () => void }
|
||||||
/** @internal */
|
|
||||||
_payloadCache?: Record<string, Promise<Record<string, any>> | Record<string, any> | null>
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_appConfig: AppConfig
|
_appConfig: AppConfig
|
||||||
|
@ -3,8 +3,9 @@ import { loadPayload } from '../composables/payload'
|
|||||||
import { onNuxtReady } from '../composables/ready'
|
import { onNuxtReady } from '../composables/ready'
|
||||||
import { useRouter } from '../composables/router'
|
import { useRouter } from '../composables/router'
|
||||||
import { getAppManifest } from '../composables/manifest'
|
import { getAppManifest } from '../composables/manifest'
|
||||||
|
|
||||||
// @ts-expect-error virtual file
|
// @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({
|
export default defineNuxtPlugin({
|
||||||
name: 'nuxt:payload',
|
name: 'nuxt:payload',
|
||||||
@ -13,11 +14,22 @@ export default defineNuxtPlugin({
|
|||||||
if (import.meta.dev) { return }
|
if (import.meta.dev) { return }
|
||||||
|
|
||||||
// Load payload after middleware & once final route is resolved
|
// Load payload after middleware & once final route is resolved
|
||||||
|
const staticKeysToRemove = new Set<string>()
|
||||||
useRouter().beforeResolve(async (to, from) => {
|
useRouter().beforeResolve(async (to, from) => {
|
||||||
if (to.path === from.path) { return }
|
if (to.path === from.path) { return }
|
||||||
const payload = await loadPayload(to.path)
|
const payload = await loadPayload(to.path)
|
||||||
if (!payload) { return }
|
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(() => {
|
onNuxtReady(() => {
|
||||||
@ -25,7 +37,8 @@ export default defineNuxtPlugin({
|
|||||||
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
nuxtApp.hooks.hook('link:prefetch', async (url) => {
|
||||||
const { hostname } = new URL(url, window.location.href)
|
const { hostname } = new URL(url, window.location.href)
|
||||||
if (hostname === window.location.hostname) {
|
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') {
|
if (isAppManifestEnabled && navigator.connection?.effectiveType !== 'slow-2g') {
|
||||||
|
@ -570,6 +570,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 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 crawlLinks = ${!!((ctx.nuxt as any)._nitro as Nitro).options.prerender.crawlLinks}`,
|
||||||
`export const spaLoadingTemplateOutside = ${ctx.nuxt.options.experimental.spaLoadingTemplateLocation === 'body'}`,
|
`export const spaLoadingTemplateOutside = ${ctx.nuxt.options.experimental.spaLoadingTemplateLocation === 'body'}`,
|
||||||
|
`export const purgeCachedData = ${!!ctx.nuxt.options.experimental.purgeCachedData}`,
|
||||||
].join('\n\n')
|
].join('\n\n')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -564,5 +564,30 @@ export default defineResolvers({
|
|||||||
* @see [PR #31175](https://github.com/nuxt/nuxt/pull/31175)
|
* @see [PR #31175](https://github.com/nuxt/nuxt/pull/31175)
|
||||||
*/
|
*/
|
||||||
templateImportResolution: true,
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -2068,13 +2068,14 @@ describe.skipIf(isDev() || isWindows || !isRenderingJson)('prefetching', () => {
|
|||||||
await gotoPath(page, '/prefetch')
|
await gotoPath(page, '/prefetch')
|
||||||
await page.waitForLoadState('networkidle')
|
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.click('[href="/prefetch/server-components"]')
|
||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
expect(await page.innerHTML('#async-server-component-count')).toBe('34')
|
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()
|
await page.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user