mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 17:35:57 +00:00
feat(nuxt): add experimental sharedPrerenderData option (#24894)
This commit is contained in:
parent
e44e8b35dd
commit
210a559350
@ -321,7 +321,41 @@ You can also set this to `chokidar` to watch all files in your source directory.
|
||||
```ts [nuxt.config.ts]
|
||||
export defineNuxtConfig({
|
||||
experimental: {
|
||||
watcher: 'chokidar-granular' // 'chokidar' or 'parcel' are also options
|
||||
watcher: 'chokidar-granular' // 'chokidar' or 'parcel' are also options
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## sharedPrerenderData
|
||||
|
||||
Enabling this feature automatically shares payload *data* between pages that are prerendered. This can result
|
||||
in a significant performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and
|
||||
fetch the same data in different pages.
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export defineNuxtConfig({
|
||||
experimental: {
|
||||
sharedPrerenderData: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Note that by default Nuxt will render pages concurrently, meaning this does not guarantee that data will
|
||||
not be fetched more than once.
|
||||
|
||||
It is particularly important when enabling this feature to make sure that any unique key of your data
|
||||
is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch
|
||||
data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch`
|
||||
should do this automatically for you.)
|
||||
|
||||
```ts
|
||||
// This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
|
||||
// to the data fetched, but Nuxt can't know that because it's not reflected in the key.
|
||||
const route = useRoute()
|
||||
const { data } = await useAsyncData(async () => {
|
||||
return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
})
|
||||
// Instead, you should use a key that uniquely identifies the data fetched.
|
||||
const { data } = await useAsyncData(route.params.slug, async () => {
|
||||
return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import type { Ref, WatchSource } from 'vue'
|
||||
import type { NuxtApp } from '../nuxt'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { toArray } from '../utils'
|
||||
import type { NuxtError} from './error';
|
||||
import type { NuxtError } from './error'
|
||||
import { createError } from './error'
|
||||
import { onNuxtReady } from './ready'
|
||||
|
||||
@ -17,18 +17,18 @@ export type _Transform<Input = any, Output = any> = (input: Input) => Output
|
||||
export type PickFrom<T, K extends Array<string>> = T extends Array<any>
|
||||
? T
|
||||
: T extends Record<string, any>
|
||||
? keyof T extends K[number]
|
||||
? T // Exact same keys as the target, skip Pick
|
||||
: K[number] extends never
|
||||
? T
|
||||
: Pick<T, K[number]>
|
||||
: T
|
||||
? keyof T extends K[number]
|
||||
? T // Exact same keys as the target, skip Pick
|
||||
: K[number] extends never
|
||||
? T
|
||||
: Pick<T, K[number]>
|
||||
: T
|
||||
|
||||
export type KeysOf<T> = Array<
|
||||
T extends T // Include all keys of union types, not just common keys
|
||||
? keyof T extends string
|
||||
? keyof T
|
||||
: never
|
||||
? keyof T
|
||||
: never
|
||||
: never
|
||||
>
|
||||
|
||||
@ -135,19 +135,29 @@ export function useAsyncData<
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [key, handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
|
||||
let [key, _handler, options = {}] = args as [string, (ctx?: NuxtApp) => Promise<ResT>, AsyncDataOptions<ResT, DataT, PickKeys, DefaultT>]
|
||||
|
||||
// Validate arguments
|
||||
if (typeof key !== 'string') {
|
||||
throw new TypeError('[nuxt] [asyncData] key must be a string.')
|
||||
}
|
||||
if (typeof handler !== 'function') {
|
||||
if (typeof _handler !== 'function') {
|
||||
throw new TypeError('[nuxt] [asyncData] handler must be a function.')
|
||||
}
|
||||
|
||||
// Setup nuxt instance payload
|
||||
const nuxt = useNuxtApp()
|
||||
|
||||
// When prerendering, share payload data automatically between requests
|
||||
const handler = import.meta.client || !import.meta.prerender || !nuxt.ssrContext?._sharedPrerenderCache ? _handler : async () => {
|
||||
const value = await nuxt.ssrContext!._sharedPrerenderCache!.get(key)
|
||||
if (value) { return value as ResT }
|
||||
|
||||
const promise = nuxt.runWithContext(_handler)
|
||||
nuxt.ssrContext!._sharedPrerenderCache!.set(key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
// Used to get default values
|
||||
const getDefault = () => null
|
||||
const getDefaultCachedData = () => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]
|
||||
|
@ -69,6 +69,11 @@ export interface NuxtSSRContext extends SSRContext {
|
||||
_renderResponse?: Partial<RenderResponse>
|
||||
/** @internal */
|
||||
_payloadReducers: Record<string, (data: any) => any>
|
||||
/** @internal */
|
||||
_sharedPrerenderCache?: {
|
||||
get<T = unknown> (key: string): Promise<T>
|
||||
set<T> (key: string, value: Promise<T>): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
export interface NuxtPayload {
|
||||
|
@ -218,6 +218,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
'process.env.NUXT_JSON_PAYLOADS': !!nuxt.options.experimental.renderJsonPayloads,
|
||||
'process.env.NUXT_COMPONENT_ISLANDS': !!nuxt.options.experimental.componentIslands,
|
||||
'process.env.NUXT_ASYNC_CONTEXT': !!nuxt.options.experimental.asyncContext,
|
||||
'process.env.NUXT_SHARED_DATA': !!nuxt.options.experimental.sharedPrerenderData,
|
||||
'process.dev': nuxt.options.dev,
|
||||
__VUE_PROD_DEVTOOLS__: false
|
||||
},
|
||||
|
@ -183,6 +183,21 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
||||
const payloadCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:payload') : null
|
||||
const islandCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island') : null
|
||||
const islandPropCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
|
||||
const sharedPrerenderPromises = import.meta.prerender && process.env.NUXT_SHARED_DATA ? new Map<string, Promise<any>>() : null
|
||||
const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA ? {
|
||||
get <T = unknown>(key: string): Promise<T> {
|
||||
if (sharedPrerenderPromises!.has(key)) {
|
||||
return sharedPrerenderPromises!.get(key)!
|
||||
}
|
||||
return useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise<T>
|
||||
},
|
||||
async set <T>(key: string, value: Promise<T>) {
|
||||
sharedPrerenderPromises!.set(key, value)
|
||||
return useStorage('internal:nuxt:prerender:shared').setItem(key, await value as any)
|
||||
// free up memory after the promise is resolved
|
||||
.finally(() => sharedPrerenderPromises!.delete(key))
|
||||
},
|
||||
} : null
|
||||
|
||||
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
||||
// TODO: Strict validation for url
|
||||
@ -287,6 +302,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
islandContext
|
||||
}
|
||||
|
||||
if (import.meta.prerender && process.env.NUXT_SHARED_DATA) {
|
||||
ssrContext._sharedPrerenderCache = sharedPrerenderCache!
|
||||
}
|
||||
|
||||
// Whether we are prerendering route
|
||||
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !isRenderingIsland
|
||||
const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(useRuntimeConfig().app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? '_payload.json' : '_payload.js') : undefined
|
||||
|
@ -259,6 +259,33 @@ export default defineUntypedSchema({
|
||||
*/
|
||||
inlineRouteRules: false,
|
||||
|
||||
/**
|
||||
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant
|
||||
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same
|
||||
* data in different pages.
|
||||
*
|
||||
* Note that by default Nuxt will render pages concurrently, meaning this does not guarantee that data will
|
||||
* not be fetched more than once.
|
||||
*
|
||||
* It is particularly important when enabling this feature to make sure that any unique key of your data
|
||||
* is always resolvable to the same data. For example, if you are using `useAsyncData` to fetch
|
||||
* data related to a particular page, you should provide a key that uniquely matches that data. (`useFetch`
|
||||
* should do this automatically for you.)
|
||||
* @example
|
||||
* ```ts
|
||||
* // This would be unsafe in a dynamic page (e.g. `[slug].vue`) because the route slug makes a difference
|
||||
* // to the data fetched, but Nuxt can't know that because it's not reflected in the key.
|
||||
* const route = useRoute()
|
||||
* const { data } = await useAsyncData(async () => {
|
||||
* return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
* })
|
||||
* // Instead, you should use a key that uniquely identifies the data fetched.
|
||||
* const { data } = await useAsyncData(route.params.slug, async () => {
|
||||
* return await $fetch(`/api/my-page/${route.params.slug}`)
|
||||
* })
|
||||
*/
|
||||
sharedPrerenderData: false,
|
||||
|
||||
/**
|
||||
* This allows specifying the default options for core Nuxt components and composables.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user